A Bluetooth LE peripheral exposes its data through a GATT server — the side of the connection that owns the attribute database, accepts read and write requests, and pushes notifications and indications to subscribed centrals. Everything a central can find on a BLE device lives somewhere in that database: a service, a characteristic, or a descriptor underneath. The Bluetooth Framework implements the full GATT server role on Windows with a single API that works across .NET, C++, and VCL.

GATT server structure: a peripheral exposing a service with six characteristics, connected to a central

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 to it, and provide resources to clients. The GATT client article covers the central side of the same protocol; this article covers the peripheral side. Both roles are implemented by the same framework and share the same service, characteristic, and descriptor model described in the Generic Attributes section of the client article.

The Bluetooth Framework supports the GATT server role with Microsoft and the BLED112 USB dongle. On Windows, the peripheral role requires a Bluetooth adapter and a driver that exposes the GattServiceProvider API — typically Windows 10 version 1703 or newer with the Microsoft Bluetooth driver. Applications that must run on older Windows versions can use the BLED112 dongle instead.


Local service and characteristics

A GATT server starts with an empty database. The application populates it with one or more services, and each service with a set of characteristics. A characteristic declares a type (its UUID), a set of properties that tell the client which operations it supports, and — for user-facing characteristics — a user description that a client can read to learn what the characteristic is for.

The Bluetooth Framework exposes service and characteristic creation through the wclGattServer class and its helper classes wclGattLocalService and wclGattLocalCharacteristic. The GattServer sample application that ships with the library demonstrates the full workflow and defines a small but complete example service:

  • Service 0xFFF0 — the demo service itself.
  • Characteristic 0xFFF1 — no properties. A placeholder characteristic that cannot be read, written, or subscribed to. It exists only to show that a characteristic without any property flags is allowed.
  • Characteristic 0xFFF2 — cpReadable. A short readable characteristic that returns 5 bytes of demo data.
  • Characteristic 0xFFF3 — cpReadable. A long readable characteristic that returns a 1024-byte buffer and demonstrates how a read request with a non-zero offset is handled.
  • Characteristic 0xFFF4 — cpWritable and cpWritableWithoutResponse. Accepts both write-with-response and write-without-response requests.
  • Characteristic 0xFFF5 — cpNotifiable. A client can subscribe to it and receive notifications.
  • Characteristic 0xFFF6 — cpIndicatable. A client can subscribe to it and receive indications, which are acknowledged by the receiver.

The property set of a characteristic is defined by the wclGattLocalCharacteristicProperty enumeration. A characteristic may combine several properties at once — for example, a single characteristic can be readable and notifiable, or writable and indicatable. The framework applies the properties exactly as specified and does not add any implicit capabilities.


Starting the server

Starting a GATT server is a multi-step procedure: open the Bluetooth manager, get a working LE radio, initialize the server, add a service, add characteristics, and finally start the server. Each step can fail independently, and the GattServer sample application checks every return code so that a problem at any stage is reported clearly and the already-allocated resources are released.

Opening the manager and getting an LE radio

Communication begins with the wclBluetoothManager class. Open it, then ask for a radio that supports Bluetooth LE with the GetLeRadio method — the same method used in the GATT client article. If a working LE radio is found, the framework stores it in a wclBluetoothRadio object that the rest of the application reuses. If no LE radio is available, the manager is closed and the application reports the failure.

Trace('Opening Bluetooth Manager');
Res := wclBluetoothManager.Open;
if Res <> WCL_E_SUCCESS then
  Trace('Open Bluetooth Manager', Res)

else begin
  Trace('Get working LE Radio');
  Res := wclBluetoothManager.GetLeRadio(Radio);
  if Res <> WCL_E_SUCCESS then
    Trace('Get working LE Radio', Res)

  else begin
    Trace('Working radio found. Use ' + Radio.ApiName + ' radio');
    Result := True;
  end;

  if not Result then begin
    Trace('Closing Bluetooth Manager');
    wclBluetoothManager.Close;
  end;
end;
Trace("Opening Bluetooth Manager");
int Res = wclBluetoothManager->Open();
if (Res != WCL_E_SUCCESS)
    Trace("Open Bluetooth Manager", Res);
else
{
    Trace("Get working LE Radio");
    Res = wclBluetoothManager->GetLeRadio(Radio);
    if (Res != WCL_E_SUCCESS)
        Trace("Get working LE Radio", Res);
    else
    {
        Trace("Working radio found. Use " + Radio->ApiName + " radio");
        Result = true;
    }

    if (!Result)
    {
        Trace("Closing Bluetooth Manager");
        wclBluetoothManager->Close();
    }
}
Trace("Opening Bluetooth Manager");
Int32 Res = FManager.Open();
if (Res != wclErrors.WCL_E_SUCCESS)
{
    Trace("Open Bluetooth Manager", Res);
    return false;
}

Trace("Get working LE Radio");
Res = FManager.GetLeRadio(out Radio);
if (Res != wclErrors.WCL_E_SUCCESS)
{
    Trace("Get working LE Radio", Res);
    FManager.Close();
    return false;
}

Trace("Working radio found. Use " + Radio.ApiName + " radio");
return true;
Trace("Opening Bluetooth Manager")
Dim Res As Int32 = FManager.Open()
If Res <> wclErrors.WCL_E_SUCCESS Then
    Trace("Open Bluetooth Manager", Res)
    Return False
End If

Trace("Get working LE Radio")
Res = FManager.GetLeRadio(Radio)
If Res <> wclErrors.WCL_E_SUCCESS Then
    Trace("Get working LE Radio", Res)
    FManager.Close()
    Return False
End If

Trace("Working radio found. Use " + Radio.ApiName + " radio")
Return True
Radio = NULL;

Trace(_T("Opening Bluetooth Manager"));
int Res = FManager.Open();
if (Res != WCL_E_SUCCESS)
{
    Trace(_T("Open Bluetooth Manager"), Res);
    return false;
}

Trace(_T("Get working LE Radio"));
Res = FManager.GetLeRadio(Radio);
if (Res != WCL_E_SUCCESS)
{
    Trace(_T("Get working LE Radio"), Res);

    Trace(_T("Closing Bluetooth Manager"));
    FManager.Close();

    return false;
}

Trace(_T("Working radio found. Use ") + CString(Radio->ApiName.c_str()) + _T(" radio"));
return true;

Initializing the server

Before anything can be added to the database, the wclGattServer object must be initialized with the LE radio obtained in the previous step. The Initialize method binds the server to that radio and prepares the underlying stack. Until Initialize succeeds, the server's Initialized property returns false and any attempt to add services fails.

Trace('Initializing GATT server');
Res := wclGattServer.Initialize(Radio);
if Res <> WCL_E_SUCCESS then begin
  Trace('GATT server initialization', Res);
  Result := False;

end else begin
  Trace('GATT server initialized');
  Result := True;
end;
Trace("Initializing GATT server");
int Res = wclGattServer->Initialize(Radio);
if (Res != WCL_E_SUCCESS)
{
    Trace("GATT server initialization", Res);
    return false;
}

Trace("GATT server initialized");
return true;
Trace("Initializing GATT server");
Int32 Res = FServer.Initialize(Radio);
if (Res != wclErrors.WCL_E_SUCCESS)
{
    Trace("GATT server initialization", Res);
    return false;
}

Trace("GATT server initialized");
return true;
Trace("Initializing GATT server")
Dim Res As Int32 = FServer.Initialize(Radio)
If Res <> wclErrors.WCL_E_SUCCESS Then
    Trace("GATT server initialization", Res)
    Return False
End If

Trace("GATT server initialized")
Return True
Trace(_T("Initializing GATT server"));
int Res = FServer.Initialize(Radio);
if (Res != WCL_E_SUCCESS)
{
    Trace(_T("GATT server initialization"), Res);
    return false;
}

Trace(_T("GATT server initialized"));
return true;

Adding a service

A service is identified by its UUID. Both short 16-bit UUIDs and long 128-bit UUIDs are supported. The sample uses a short UUID (0xFFF0) because the service is a private test service and does not need to allocate a vendor-specific 128-bit ID. The AddService method receives the UUID and returns a wclGattLocalService object that represents the newly created service and is used to add characteristics to it.

Trace('Adding GATT service');

Uuid.IsShortUuid := True;
Uuid.ShortUuid := $FFF0;
Res := wclGattServer.AddService(Uuid, Result);
if Res <> WCL_E_SUCCESS then
  Trace(4, 'Add GATT service', Res)
else
  Trace('GATT service added');
Trace("Adding GATT service");

TwclGattUuid Uuid;
Uuid.IsShortUuid = true;
Uuid.ShortUuid = 0xFFF0;
TwclGattLocalService* Result = NULL;
int Res = wclGattServer->AddService(Uuid, Result);
if (Res != WCL_E_SUCCESS)
    Trace(4, "Add GATT service", Res);
else
    Trace("GATT service added");
Trace("Adding GATT service");

wclGattUuid Uuid = new wclGattUuid();
Uuid.IsShortUuid = true;
Uuid.ShortUuid = 0xFFF0;
wclGattLocalService Result;
Int32 Res = FServer.AddService(Uuid, out Result);
if (Res != wclErrors.WCL_E_SUCCESS)
    Trace(4, "Add GATT service", Res);
else
    Trace("GATT service added");
return Result;
Trace("Adding GATT service")

Dim Uuid As wclGattUuid = New wclGattUuid()
Uuid.IsShortUuid = True
Uuid.ShortUuid = &HFFF0
Dim Result As wclGattLocalService = Nothing
Dim Res As Int32 = FServer.AddService(Uuid, Result)
If Res <> wclErrors.WCL_E_SUCCESS Then
    Trace(4, "Add GATT service", Res)
Else
    Trace("GATT service added")
End If
Return Result
Trace(_T("Adding GATT service"));

wclGattUuid Uuid;
Uuid.IsShortUuid = true;
Uuid.ShortUuid = 0xFFF0;
CwclGattLocalService* Result;
int Res = FServer.AddService(Uuid, Result);
if (Res != WCL_E_SUCCESS)
{
    Trace(4, _T("Add GATT service"), Res);
    return NULL;
}

Trace(_T("GATT service added"));
return Result;

Adding characteristics

Each characteristic belongs to a service and is created with the AddCharacteristic method of the wclGattLocalService class. The method takes a UUID and a wclGattLocalCharacteristicParameters record that carries the characteristic's properties, its optional descriptors, its optional presentation formats, and its user-visible description.

The property set is stored in the Props field. In the .NET editions it is a bitwise combination of wclGattLocalCharacteristicProperty flags; in the C++, VCL, and C++ Builder editions it is a set. The framework uses the same flag names in every edition — cpReadable, cpWritable, cpWritableWithoutResponse, cpNotifiable, and cpIndicatable — so that the code reads identically across languages.

The sample creates six characteristics in the same service, one for each interesting combination of properties. An empty characteristic with no properties, a short readable one, a long readable one, a writable one, a notifiable one, and an indicatable one. Each call returns a wclGattLocalCharacteristic object that can later be used to send notifications or indications.

Trace(4, 'Adding GATT characteristics');

Uuid.IsShortUuid := True;
Params.Descriptors := nil;
Params.PresentationFormats := nil;

// Empty characteristic.
Uuid.ShortUuid := $FFF1;
Params.Props := [];
Params.UserDescription := 'Empty characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);

// Short readable characteristic.
Uuid.ShortUuid := $FFF2;
Params.Props := [cpReadable];
Params.UserDescription := 'Short readable characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);

// Long readable characteristic.
Uuid.ShortUuid := $FFF3;
Params.Props := [cpReadable];
Params.UserDescription := 'Long readable characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);

// Writable characteristic.
Uuid.ShortUuid := $FFF4;
Params.Props := [cpWritable, cpWritableWithoutResponse];
Params.UserDescription := 'Writable characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);

// Notifiable characteristic.
Uuid.ShortUuid := $FFF5;
Params.Props := [cpNotifiable];
Params.UserDescription := 'Notifiable characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);

// Indicatable characteristic.
Uuid.ShortUuid := $FFF6;
Params.Props := [cpIndicatable];
Params.UserDescription := 'Indicatable characteristic';
Res := Service.AddCharacteristic(Uuid, Params, Char);
Trace(4, "Adding GATT characteristics");

TwclGattLocalCharacteristic* Char;

TwclGattUuid Uuid;
Uuid.IsShortUuid = true;

TwclGattLocalCharacteristicParameters Params;
Params.Descriptors.Length = 0;
Params.PresentationFormats.Length = 0;

// Empty characteristic.
Uuid.ShortUuid = 0xFFF1;
Params.Props.Clear();
Params.UserDescription = "Empty characteristic";
int Res = Service->AddCharacteristic(Uuid, Params, Char);

// Short readable characteristic.
Uuid.ShortUuid = 0xFFF2;
Params.Props << cpReadable;
Params.UserDescription = "Short readable characteristic";
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Long readable characteristic.
Uuid.ShortUuid = 0xFFF3;
Params.Props.Clear();
Params.Props << cpReadable;
Params.UserDescription = "Long readable characteristic";
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Writable characteristic.
Uuid.ShortUuid = 0xFFF4;
Params.Props.Clear();
Params.Props << cpWritable << cpWritableWithoutResponse;
Params.UserDescription = "Writable characteristic";
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Notifiable characteristic.
Uuid.ShortUuid = 0xFFF5;
Params.Props.Clear();
Params.Props << cpNotifiable;
Params.UserDescription = "Notifiable characteristic";
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Indicatable characteristic.
Uuid.ShortUuid = 0xFFF6;
Params.Props.Clear();
Params.Props << cpIndicatable;
Params.UserDescription = "Indicatable characteristic";
Res = Service->AddCharacteristic(Uuid, Params, Char);
Trace(4, "Adding GATT characteristics");

wclGattLocalCharacteristic Char;

wclGattUuid Uuid = new wclGattUuid();
Uuid.IsShortUuid = true;

wclGattLocalCharacteristicParameters Params = new wclGattLocalCharacteristicParameters();
Params.Descriptors = null;
Params.PresentationFormats = null;

// Empty characteristic.
Uuid.ShortUuid = 0xFFF1;
Params.Props = 0;
Params.UserDescription = "Empty characteristic";
Int32 Res = Service.AddCharacteristic(Uuid, Params, out Char);

// Short readable characteristic.
Uuid.ShortUuid = 0xFFF2;
Params.Props = wclGattLocalCharacteristicProperty.cpReadable;
Params.UserDescription = "Short readable characteristic";
Res = Service.AddCharacteristic(Uuid, Params, out Char);

// Long readable characteristic.
Uuid.ShortUuid = 0xFFF3;
Params.Props = wclGattLocalCharacteristicProperty.cpReadable;
Params.UserDescription = "Long readable characteristic";
Res = Service.AddCharacteristic(Uuid, Params, out Char);

// Writable characteristic.
Uuid.ShortUuid = 0xFFF4;
Params.Props = wclGattLocalCharacteristicProperty.cpWritable |
    wclGattLocalCharacteristicProperty.cpWritableWithoutResponse;
Params.UserDescription = "Writable characteristic";
Res = Service.AddCharacteristic(Uuid, Params, out Char);

// Notifiable characteristic.
Uuid.ShortUuid = 0xFFF5;
Params.Props = wclGattLocalCharacteristicProperty.cpNotifiable;
Params.UserDescription = "Notifiable characteristic";
Res = Service.AddCharacteristic(Uuid, Params, out Char);

// Indicatable characteristic.
Uuid.ShortUuid = 0xFFF6;
Params.Props = wclGattLocalCharacteristicProperty.cpIndicatable;
Params.UserDescription = "Indicatable characteristic";
Res = Service.AddCharacteristic(Uuid, Params, out Char);
Trace(4, "Adding GATT characteristics")

Dim [Char] As wclGattLocalCharacteristic = Nothing
Dim Uuid As wclGattUuid = New wclGattUuid()
Uuid.IsShortUuid = True

Dim Params As wclGattLocalCharacteristicParameters = New wclGattLocalCharacteristicParameters()
Params.Descriptors = Nothing
Params.PresentationFormats = Nothing

' Empty characteristic.
Uuid.ShortUuid = &HFFF1
Params.Props = 0
Params.UserDescription = "Empty characteristic"
Dim Res As Int32 = Service.AddCharacteristic(Uuid, Params, [Char])

' Short readable characteristic.
Uuid.ShortUuid = &HFFF2
Params.Props = wclGattLocalCharacteristicProperty.cpReadable
Params.UserDescription = "Short readable characteristic"
Res = Service.AddCharacteristic(Uuid, Params, [Char])

' Long readable characteristic.
Uuid.ShortUuid = &HFFF3
Params.Props = wclGattLocalCharacteristicProperty.cpReadable
Params.UserDescription = "Long readable characteristic"
Res = Service.AddCharacteristic(Uuid, Params, [Char])

' Writable characteristic.
Uuid.ShortUuid = &HFFF4
Params.Props = wclGattLocalCharacteristicProperty.cpWritable Or wclGattLocalCharacteristicProperty.cpWritableWithoutResponse
Params.UserDescription = "Writable characteristic"
Res = Service.AddCharacteristic(Uuid, Params, [Char])

' Notifiable characteristic.
Uuid.ShortUuid = &HFFF5
Params.Props = wclGattLocalCharacteristicProperty.cpNotifiable
Params.UserDescription = "Notifiable characteristic"
Res = Service.AddCharacteristic(Uuid, Params, [Char])

' Indicatable characteristic.
Uuid.ShortUuid = &HFFF6
Params.Props = wclGattLocalCharacteristicProperty.cpIndicatable
Params.UserDescription = "Indicatable characteristic"
Res = Service.AddCharacteristic(Uuid, Params, [Char])
Trace(4, _T("Adding GATT characteristics"));

wclGattUuid Uuid;
Uuid.IsShortUuid = true;

wclGattLocalCharacteristicParameters Params;
Params.Descriptors.clear();
Params.PresentationFormats.clear();

CwclGattLocalCharacteristic* Char;

// Empty characteristic.
Uuid.ShortUuid = 0xFFF1;
Params.Props.clear();
Params.UserDescription = _T("Empty characteristic");
int Res = Service->AddCharacteristic(Uuid, Params, Char);

// Short readable characteristic.
Uuid.ShortUuid = 0xFFF2;
Params.Props.insert(cpReadable);
Params.UserDescription = _T("Short readable characteristic");
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Long readable characteristic.
Uuid.ShortUuid = 0xFFF3;
Params.Props.clear();
Params.Props.insert(cpReadable);
Params.UserDescription = _T("Long readable characteristic");
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Writable characteristic.
Uuid.ShortUuid = 0xFFF4;
Params.Props.clear();
Params.Props.insert(cpWritable);
Params.Props.insert(cpWritableWithoutResponse);
Params.UserDescription = _T("Writable characteristic");
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Notifiable characteristic.
Uuid.ShortUuid = 0xFFF5;
Params.Props.clear();
Params.Props.insert(cpNotifiable);
Params.UserDescription = _T("Notifiable characteristic");
Res = Service->AddCharacteristic(Uuid, Params, Char);

// Indicatable characteristic.
Uuid.ShortUuid = 0xFFF6;
Params.Props.clear();
Params.Props.insert(cpIndicatable);
Params.UserDescription = _T("Indicatable characteristic");
Res = Service->AddCharacteristic(Uuid, Params, Char);

Starting the server

Once the database is populated, the Start method begins advertising and makes the server visible to nearby BLE centrals. When the server has entered the running state, the framework fires the OnStarted event — the appropriate place to update the user interface, log the state change, or begin accepting connections. If the framework cannot start the server, the error code returned by Start is reported and the already-allocated service and characteristic objects are released by calling Uninitialize.

Radio := nil;
if InitializeBluetooth(Radio) then begin
  BoolRes := InitializeServer(Radio);
  if BoolRes then begin
    Service := AddService;
    if Service = nil then
      BoolRes := False

    else begin
      BoolRes := AddCharacteristics(Service);
      if BoolRes then begin
        Trace('Starting GATT server');
        Res := wclGattServer.Start;
        if Res <> WCL_E_SUCCESS then begin
          Trace('GATT server start', Res);
          BoolRes := False;
        end;
      end;
    end;

    if not BoolRes then
      UninitializeServer;
  end;

  if not BoolRes then
    UninitializeBluetooth;
end;
TwclBluetoothRadio* Radio = NULL;
if (InitializeBluetooth(Radio))
{
    bool BoolRes = InitializeServer(Radio);
    if (BoolRes)
    {
        TwclGattLocalService* Service = AddService();
        if (Service == NULL)
            BoolRes = false;
        else
        {
            BoolRes = AddCharacteristics(Service);
            if (BoolRes)
            {
                Trace("Starting GATT server");
                int Res = wclGattServer->Start();
                if (Res != WCL_E_SUCCESS)
                {
                    Trace("GATT server start", Res);
                    BoolRes = false;
                }
            }
        }
        if (!BoolRes)
            UninitializeServer();
    }
    if (!BoolRes)
        UninitializeBluetooth();
}
wclBluetoothRadio Radio = null;
if (InitializeBluetooth(out Radio))
{
    Boolean BoolRes = InitializeServer(Radio);
    if (BoolRes)
    {
        wclGattLocalService Service = AddService();
        if (Service == null)
            BoolRes = false;
        else
        {
            BoolRes = AddCharacteristics(Service);
            if (BoolRes)
            {
                Trace("Starting GATT server");
                Int32 Res = FServer.Start();
                if (Res != wclErrors.WCL_E_SUCCESS)
                {
                    Trace("GATT server start", Res);
                    BoolRes = false;
                }
            }
        }
        if (!BoolRes)
            UninitializeServer();
    }
    if (!BoolRes)
        UninitializeBluetooth();
}
Dim Radio As wclBluetoothRadio = Nothing
If InitializeBluetooth(Radio) Then
    Dim BoolRes As Boolean = InitializeServer(Radio)
    If BoolRes Then
        Dim Service As wclGattLocalService = AddService()
        If Service Is Nothing Then
            BoolRes = False
        Else
            BoolRes = AddCharacteristics(Service)
            If BoolRes Then
                Trace("Starting GATT server")
                Dim Res As Int32 = FServer.Start()
                If Res <> wclErrors.WCL_E_SUCCESS Then
                    Trace("GATT server start", Res)
                    BoolRes = False
                End If
            End If
        End If
        If Not BoolRes Then UninitializeServer()
    End If
    If Not BoolRes Then UninitializeBluetooth()
End If
CwclBluetoothRadio* Radio = NULL;
if (InitializeBluetooth(Radio))
{
    bool BoolRes = InitializeServer(Radio);
    if (BoolRes)
    {
        CwclGattLocalService* Service = AddService();
        if (Service == NULL)
            BoolRes = false;
        else
        {
            BoolRes = AddCharacteristics(Service);
            if (BoolRes)
            {
                Trace(_T("Starting GATT server"));
                int Res = FServer.Start();
                if (Res != WCL_E_SUCCESS)
                {
                    Trace(_T("GATT server start"), Res);
                    BoolRes = false;
                }
            }
        }
        if (!BoolRes)
            UninitializeServer();
    }
    if (!BoolRes)
        UninitializeBluetooth();
}

Handling clients

Once the server is running, the framework delivers real client connection events — a capability the standard Windows UWP GATT provider does not offer. There is no need to wait for a client to read, write, or subscribe to a characteristic; the server knows that a central is connected the moment the physical link is established. See GATT connection state change detection for the background on why this matters.

Connection events

The OnClientConnected event fires when a central establishes a link, and OnClientDisconnected fires when the link is dropped. Each event carries a wclGattServerClient object that represents the connected client. That object exposes the client's Bluetooth address and a set of per-client methods that are used throughout the rest of the article to read connection parameters, read PHY, get the maximum PDU size, get the maximum notification size, and disconnect the client on demand.

The GattServer sample application records every connected client in a list and, on disconnection, removes it from the list. Characteristics a client had subscribed to are automatically removed from the list before the disconnection event fires, so the disconnection handler only needs to remove the client row.

procedure TfmMain.wclGattServerClientConnected(Sender: TObject;
  const Client: TwclGattServerClient);
begin
  Trace('Client connected: ' + IntToHex(Client.Address, 12));

  Item := lvDevices.Items.Add;
  Item.Caption := IntToHex(Client.Address, 12);
  Item.Data := Client;
  // ...fill remaining columns with empty strings...

  UpdateMaxPduSize(Client);
  UpdateConnectionParams(Client);
  UpdatePhy(Client);
  UpdateNotificationSize(Client);
end;

procedure TfmMain.wclGattServerClientDisconnected(Sender: TObject;
  const Client: TwclGattServerClient);
begin
  Trace('Client disconnected: ' + IntToHex(Client.Address, 12));

  Item := FindItem(Client);
  if Item <> nil then begin
    lvDevices.Items.Delete(Item.Index);

    UpdateNotifyAllStatus;
  end;
end;
void __fastcall TfmMain::wclGattServerClientConnected(TObject *Sender,
      const TwclGattServerClient *Client)
{
    Trace("Client connected: " + IntToHex(Client->Address, 12));

    TListItem* Item = lvDevices->Items->Add();
    Item->Caption = IntToHex(Client->Address, 12);
    Item->Data = (void*)Client;
    // ...fill remaining columns with empty strings...

    UpdateMaxPduSize((TwclGattServerClient*)Client);
    UpdateConnectionParams((TwclGattServerClient*)Client);
    UpdatePhy((TwclGattServerClient*)Client);
    UpdateNotificationSize((TwclGattServerClient*)Client);
}

void __fastcall TfmMain::wclGattServerClientDisconnected(TObject *Sender,
      const TwclGattServerClient *Client)
{
    Trace("Client disconnected: " + IntToHex(Client->Address, 12));

    TListItem* Item = FindItem((TwclGattServerClient*)Client);
    if (Item != NULL)
    {
        lvDevices->Items->Delete(Item->Index);

        UpdateNotifyAllStatus();
    }
}
void GettServerClientConnected(object Sender, wclGattServerClient Client)
{
    Trace("Client connected: " + Client.Address.ToString("X12"));

    ListViewItem Item = lvDevices.Items.Add(Client.Address.ToString("X12"));
    Item.Tag = Client;
    // ...fill remaining columns with empty strings...

    UpdateMaxPduSize(Client);
    UpdateConnectionParams(Client);
    UpdatePhy(Client);
    UpdateNotificationSize(Client);
}

void GattServerClientDisconnected(object Sender, wclGattServerClient Client)
{
    Trace("Client disconnected: " + Client.Address.ToString("X12"));

    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        lvDevices.Items.Remove(Item);

        UpdateNotifyAllStatus();
    }
}
Private Sub FServer_OnClientConnected(Sender As Object, Client As wclBluetooth.wclGattServerClient) Handles FServer.OnClientConnected
    Trace("Client connected: " + Client.Address.ToString("X12"))

    Dim Item As ListViewItem = lvDevices.Items.Add(Client.Address.ToString("X12"))
    Item.Tag = Client
    ' ...fill remaining columns with empty strings...

    UpdateMaxPduSize(Client)
    UpdateConnectionParams(Client)
    UpdatePhy(Client)
    UpdateNotificationSize(Client)
End Sub

Private Sub FServer_OnClientDisconnected(Sender As Object, Client As wclBluetooth.wclGattServerClient) Handles FServer.OnClientDisconnected
    Trace("Client disconnected: " + Client.Address.ToString("X12"))

    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        lvDevices.Items.Remove(Item)

        UpdateNotifyAllStatus()
    End If
End Sub
void CGattServerDlg::GattServerClientConnected(void* Sender,
    CwclGattServerClient* const Client)
{
    Trace(_T("Client connected: ") + IntToHex(Client->Address));

    int Ndx = lvDevices.GetItemCount();
    lvDevices.InsertItem(Ndx, IntToHex(Client->Address));
    lvDevices.SetItemData(Ndx, (DWORD_PTR)Client);
    // ...fill remaining columns with empty strings...

    UpdateMaxPduSize(Client);
    UpdateConnectionParams(Client);
    UpdatePhy(Client);
    UpdateNotificationSize(Client);
}

void CGattServerDlg::GattServerClientDisconnected(void* Sender,
    CwclGattServerClient* const Client)
{
    Trace(_T("Client disconnected: ") + IntToHex(Client->Address));

    int Item = FindItem(Client);
    if (Item > -1)
    {
        lvDevices.DeleteItem(Item);

        UpdateNotifyAllStatus();
    }
}

Disconnecting a client

The server can also force a client to disconnect. The Disconnect method of the wclGattServerClient class terminates the link at the radio level and the client is removed from the server's list through the normal OnClientDisconnected event. This is the same operation that the framework performs when a central drops the connection on its own, except that the server initiates it.

Force-disconnecting a client is useful for enforcing policies such as a maximum session length, an allow-list check performed after the connection is established, or a firmware update that requires exclusive access to the radio. The standard UWP GATT provider does not expose this capability.

Res := TwclGattServerClient(lvDevices.Selected.Data).Disconnect;
if Res <> WCL_E_SUCCESS then
  ShowMessage('Disconnect error: 0x' + IntToHex(Res, 8));
int Res = ((TwclGattServerClient*)lvDevices->Selected->Data)->Disconnect();
if (Res != WCL_E_SUCCESS)
    ShowMessage("Disconnect error: 0x" + IntToHex(Res, 8));
Int32 Res = ((wclGattServerClient)lvDevices.SelectedItems[0].Tag).Disconnect();
if (Res != wclErrors.WCL_E_SUCCESS)
    MessageBox.Show("Disconnect error: 0x" + Res.ToString("X8"));
Dim Res As Int32 = CType(lvDevices.SelectedItems(0).Tag, wclGattServerClient).Disconnect()
If Res <> wclErrors.WCL_E_SUCCESS Then MessageBox.Show("Disconnect error: 0x" + Res.ToString("X8"))
int Res = ((CwclGattServerClient*)lvDevices.GetItemData(Item))->Disconnect();
if (Res != WCL_E_SUCCESS)
    AfxMessageBox(_T("Disconnect error: 0x") + IntToHex(Res));

Reading and writing

Read and write requests from a client are delivered as events. Each request is a transaction that the server must complete by calling a Respond or a RespondWithError method on the request object. Until the request is completed, the client remains blocked waiting for the answer, so the handler should respond as quickly as possible — heavy processing belongs outside the event.

Read requests

The OnRead event fires when a client reads a characteristic value. The event provides the client, the characteristic, and a wclGattLocalCharacteristicReadRequest object that carries three pieces of information: the requested offset within the characteristic value, the maximum size of the response the client will accept, and a set of methods that the server uses to complete the request.

A read request may arrive with a non-zero offset. This happens when a client reads a characteristic whose value is longer than the maximum PDU size negotiated for the link: the client reads the value in chunks, and each chunk arrives with a different offset. The server is responsible for returning the correct slice of its stored value in response to each request.

The GattServer sample demonstrates both cases. A read of the short characteristic (0xFFF2) returns a fixed 5-byte array. A read of the long characteristic (0xFFF3) returns the remainder of a 1024-byte buffer, starting at the requested offset. Both responses are delivered through the Respond method. If the framework or the application cannot fulfil the request, the server responds with an error through RespondWithError — the sample uses WCL_E_BLUETOOTH_LE_UNLIKELY as a generic failure code.

procedure TfmMain.wclGattServerRead(Sender: TObject;
  const Client: TwclGattServerClient;
  const Characteristic: TwclGattLocalCharacteristic;
  const Request: TwclGattLocalCharacteristicReadRequest);
var
  Res: Integer;
begin
  Trace('Read request received from: ' + IntToHex(Client.Address, 12));
  Trace(4, 'Characteristic: ' + IntToHex(Characteristic.Uuid.ShortUuid, 4));
  Trace(4, 'Offset: ' + IntToStr(Request.Offset));
  Trace(4, 'Buffer size: ' + IntToStr(Request.Size));

  if Characteristic.Uuid.ShortUuid = $FFF2 then
    Res := Request.Respond(@ShortData, SHORT_DATA_LEN)
  else begin
    Res := Request.Respond(@LongData[Request.Offset],
      LONG_DATA_LEN - Request.Offset);
  end;

  if Res <> WCL_E_SUCCESS then begin
    Trace(8, 'Set data', Res);
    Trace(8, 'Respond with error');
    Res := Request.RespondWithError(WCL_E_BLUETOOTH_LE_UNLIKELY);
    if Res <> WCL_E_SUCCESS then
      Trace(12, 'Respond', Res);

  end else
    Trace(8, 'Data sent');
end;
void __fastcall TfmMain::wclGattServerRead(TObject *Sender,
      const TwclGattServerClient *Client,
      const TwclGattLocalCharacteristic *Characteristic,
      const TwclGattLocalCharacteristicReadRequest *Request)
{
    Trace("Read request received from: " + IntToHex(Client->Address, 12));
    Trace(4, "Characteristic: " + IntToHex(Characteristic->Uuid.ShortUuid, 4));
    Trace(4, "Offset: " + IntToStr((int)Request->Offset));
    Trace(4, "Buffer size: " + IntToStr((int)Request->Size));

    int Res;
    if (Characteristic->Uuid.ShortUuid == 0xFFF2)
        Res = ((TwclGattLocalCharacteristicReadRequest*)Request)->Respond(&ShortData, SHORT_DATA_LEN);
    else
    {
        Res = ((TwclGattLocalCharacteristicReadRequest*)Request)->Respond(&LongData[Request->Offset],
                LONG_DATA_LEN - Request->Offset);
    }

    if (Res != WCL_E_SUCCESS)
    {
        Trace(8, "Set data", Res);
        Trace(8, "Respond with error");
        Res = ((TwclGattLocalCharacteristicReadRequest*)Request)->RespondWithError(
                WCL_E_BLUETOOTH_LE_UNLIKELY);
        if (Res != WCL_E_SUCCESS)
            Trace(12, "Respond", Res);
    }
    else
        Trace(8, "Data sent");
}
void GattServerRead(object Sender, wclGattServerClient Client, wclGattLocalCharacteristic Characteristic,
    wclGattLocalCharacteristicReadRequest Request)
{
    Trace("Read request received from: " + Client.Address.ToString("X12"));
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"));
    Trace(4, "Offset: " + Request.Offset.ToString());
    Trace(4, "Buffer size: " + Request.Size.ToString());

    Int32 Res;
    if (Characteristic.Uuid.ShortUuid == 0xFFF2)
        Res = Request.Respond(ShortData);
    else
    {
        Byte[] ToSend = new Byte[LONG_DATA_LEN - Request.Offset];
        Array.Copy(LongData, Request.Offset, ToSend, 0, LONG_DATA_LEN - Request.Offset);
        Res = Request.Respond(ToSend);
    }

    if (Res != wclErrors.WCL_E_SUCCESS)
    {
        Trace(8, "Set data", Res);
        Trace(8, "Respond with error");
        Res = Request.RespondWithError(wclBluetoothErrors.WCL_E_BLUETOOTH_LE_UNLIKELY);
        if (Res != wclErrors.WCL_E_SUCCESS)
            Trace(12, "Respond", Res);
    }
    else
        Trace(8, "Data sent");
}
Private Sub FServer_OnRead(Sender As Object, Client As wclBluetooth.wclGattServerClient, Characteristic As wclBluetooth.wclGattLocalCharacteristic, Request As wclBluetooth.wclGattLocalCharacteristicReadRequest) Handles FServer.OnRead
    Trace("Read request received from: " + Client.Address.ToString("X12"))
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"))
    Trace(4, "Offset: " + Request.Offset.ToString())
    Trace(4, "Buffer size: " + Request.Size.ToString())

    Dim Res As Integer
    If Characteristic.Uuid.ShortUuid = &HFFF2 Then
        Res = Request.Respond(ShortData)
    Else
        Dim ToSend(LONG_DATA_LEN - Request.Offset) As Byte
        Array.Copy(LongData, Request.Offset, ToSend, 0, LONG_DATA_LEN - Request.Offset)
        Res = Request.Respond(ToSend)
    End If

    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace(8, "Set data", Res)
        Trace(8, "Respond with error")
        Res = Request.RespondWithError(wclBluetoothErrors.WCL_E_BLUETOOTH_LE_UNLIKELY)
        If Res <> wclErrors.WCL_E_SUCCESS Then Trace(12, "Respond", Res)
    Else
        Trace(8, "Data sent")
    End If
End Sub
void CGattServerDlg::GattServerRead(void* Sender,
    CwclGattServerClient* const Client,
    CwclGattLocalCharacteristic* const Characteristic,
    CwclGattLocalCharacteristicReadRequest* const Request)
{
    Trace(_T("Read request received from: ") + IntToHex(Client->Address));
    Trace(4, _T("Characteristic: ") + IntToHex(Characteristic->Uuid.ShortUuid));
    Trace(4, _T("Offset: ") + IntToStr(Request->Offset));
    Trace(4, _T("Buffer size: ") + IntToStr(Request->Size));

    int Res;
    if (Characteristic->Uuid.ShortUuid == 0xFFF2)
        Res = Request->Respond(&ShortData, SHORT_DATA_LEN);
    else
        Res = Request->Respond(&LongData[Request->Offset],
                LONG_DATA_LEN - Request->Offset);

    if (Res != WCL_E_SUCCESS)
    {
        Trace(8, _T("Set data"), Res);
        Trace(8, _T("Respond with error"));
        Res = Request->RespondWithError(WCL_E_BLUETOOTH_LE_UNLIKELY);
        if (Res != WCL_E_SUCCESS)
            Trace(12, _T("Respond"), Res);
    }
    else
        Trace(8, _T("Data sent"));
}

Write requests

The OnWrite event fires when a client writes to a characteristic. Unlike a read request, a write request may or may not expect a response. The WithResponse flag on the wclGattLocalCharacteristicWriteRequest object tells the handler which flavour of write arrived.

If WithResponse is true, the client is waiting for a confirmation. The server must call either Respond to acknowledge the write or RespondWithError to reject it. If WithResponse is false, the client does not expect an answer and the request completes as soon as the handler returns — no response method should be called in that case.

The data received by the write request is available through the Data and Size fields (or a byte array in the .NET editions), and the starting offset of the write within the characteristic value is available through the Offset field. As with reads, a long write may be split into several requests with different offsets, and the server is responsible for assembling the pieces if the characteristic's value is longer than a single PDU.

procedure TfmMain.wclGattServerWrite(Sender: TObject;
  const Client: TwclGattServerClient;
  const Characteristic: TwclGattLocalCharacteristic;
  const Request: TwclGattLocalCharacteristicWriteRequest);
var
  s: String;
  i: Cardinal;
  Res: Integer;
begin
  Trace('Write request received from: ' + IntToHex(Client.Address, 12));
  Trace(4, 'Characteristic: ' + IntToHex(Characteristic.Uuid.ShortUuid, 4));
  Trace(4, 'Size: ' + IntToStr(Request.Size));
  Trace(4, 'Offset: ' + IntToStr(Request.Offset));

  s := '';
  if (Request.Size > 0) and (Request.Data <> nil) then begin
    for i := 0 to Request.Size - 1 do
      s := s + IntToHex(Byte(PAnsiChar(Request.Data)[i]), 2);
  end;
  Trace(8, s);

  if Request.WithResponse then begin
    Trace(4, 'Sending response');
    Res := Request.Respond;
    // If data is incorrect you can answer with error using:
    //         Res := Request.RespondWithError(Error);
    // For possible error code refer to RespondWithError() method description.
    if Res <> WCL_E_SUCCESS then
      Trace(8, 'Send response', Res)
    else
      Trace(8, 'Response sent');
  end;
end;
void __fastcall TfmMain::wclGattServerWrite(TObject *Sender,
      const TwclGattServerClient *Client,
      const TwclGattLocalCharacteristic *Characteristic,
      const TwclGattLocalCharacteristicWriteRequest *Request)
{
    Trace("Write request received from: " + IntToHex(Client->Address, 12));
    Trace(4, "Characteristic: " + IntToHex(Characteristic->Uuid.ShortUuid, 4));
    Trace(4, "Size: " + IntToStr((int)Request->Size));
    Trace(4, "Offset: " + IntToStr((int)Request->Offset));

    String s = "";
    for (unsigned i = 0; i < Request->Size; i++)
        s = s + IntToHex(((Byte*)Request->Data)[i], 2);
    Trace(8, s);

    if (Request->WithResponse)
    {
        Trace(4, "Sending response");
        int Res = ((TwclGattLocalCharacteristicWriteRequest*)Request)->Respond();
        // If data is incorrect you can answer with error using:
        //         Res = Request->RespondWithError(Error);
        // For possible error code refer to RespondWithError() method
        // description.
        if (Res != WCL_E_SUCCESS)
            Trace(8, "Send response", Res);
        else
            Trace(8, "Response sent");
    }
}
void GattServerWrite(object Sender, wclGattServerClient Client, wclGattLocalCharacteristic Characteristic,
    wclGattLocalCharacteristicWriteRequest Request)
{
    Trace("Write request received from: " + Client.Address.ToString("X12"));
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"));
    if (Request.Data == null)
        Trace(4, "Size: 0");
    else
        Trace(4, "Size: " + Request.Data.Length.ToString());
    Trace(4, "Offset: " + Request.Offset.ToString());

    String s = "";
    if (Request.Data != null && Request.Data.Length > 0)
    {
        for (int i = 0; i < Request.Data.Length; i++)
            s = s + Request.Data[i].ToString("X2");
    }
    Trace(8, s);

    if (Request.WithResponse)
    {
        Trace(4, "Sending response");
        Int32 Res = Request.Respond();
        // If data is incorrect you can answer with error using:
        //         Res = Request.RespondWithError(Error);
        // For possible error code refer to RespondWithError() method description.
        if (Res != wclErrors.WCL_E_SUCCESS)
            Trace(8, "Send response", Res);
        else
            Trace(8, "Response sent");
    }
}
Private Sub FServer_OnWrite(Sender As Object, Client As wclBluetooth.wclGattServerClient, Characteristic As wclBluetooth.wclGattLocalCharacteristic, Request As wclBluetooth.wclGattLocalCharacteristicWriteRequest) Handles FServer.OnWrite
    Trace("Write request received from: " + Client.Address.ToString("X12"))
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"))
    If Request.Data Is Nothing Then
        Trace(4, "Size: 0")
    Else
        Trace(4, "Size: " + Request.Data.Length.ToString())
    End If
    Trace(4, "Offset: " + Request.Offset.ToString())

    Dim s As String = ""
    If Request.Data IsNot Nothing AndAlso Request.Data.Length > 0 Then
        For i As Integer = 0 To Request.Data.Length - 1
            s = s + Request.Data(i).ToString("X2")
        Next
    End If
    Trace(8, s)

    If Request.WithResponse Then
        Trace(4, "Sending response")
        Dim Res As Int32 = Request.Respond()
        ' If data is incorrect you can answer with error using:
        '         Res = Request.RespondWithError(Error)
        ' For possible error code refer to RespondWithError() method description.
        If Res <> wclErrors.WCL_E_SUCCESS Then
            Trace(8, "Send response", Res)
        Else
            Trace(8, "Response sent")
        End If
    End If
End Sub
void CGattServerDlg::GattServerWrite(void* Sender, CwclGattServerClient* const Client,
    CwclGattLocalCharacteristic* const Characteristic,
    CwclGattLocalCharacteristicWriteRequest* const Request)
{
    Trace(_T("Write request received from: ") + IntToHex(Client->Address));
    Trace(4, _T("Characteristic: ") + IntToHex(Characteristic->Uuid.ShortUuid));
    Trace(4, _T("Size: ") + IntToStr(Request->Size));
    Trace(4, _T("Offset: ") + IntToStr(Request->Offset));

    CString s = _T("");
    if (Request->Size > 0 && Request->Data != NULL)
    {
        for (unsigned long i = 0; i < Request->Size; i++)
            s = s + IntToHex(((PBYTE)Request->Data)[i]);
    }
    Trace(8, s);

    if (Request->WithResponse)
    {
        Trace(4, _T("Sending response"));
        int Res = Request->Respond();
        // If data is incorrect you can answer with error using:
        //         Res = Request->RespondWithError(Error);
        // For possible error code refer to RespondWithError() method description.
        if (Res != WCL_E_SUCCESS)
            Trace(8, _T("Send response"), Res);
        else
            Trace(8, _T("Response sent"));
    }
}

Notifications and indications

A characteristic with cpNotifiable or cpIndicatable set can push value changes to subscribed clients. Notifications are unacknowledged and fast; indications are acknowledged and, because the protocol forbids two consecutive indications without a confirmation in between, slower. From the server's point of view both are delivered through the same Notify method on the wclGattLocalCharacteristic class — the characteristic's property flags decide which of the two the framework actually uses on the air. The client article covers the difference in more detail in the Notifications vs indications section.

Subscriptions

The OnSubscribed event fires when a client subscribes to a characteristic, and OnUnsubscribed fires when the subscription is cancelled. The framework calls OnUnsubscribed before OnClientDisconnected when a client drops its link, so a disconnection does not leave stale subscriptions in the server's bookkeeping.

The sample tracks each subscription as a sub-row beneath the connected client's row in the list. When a client subscribes, a new row is inserted under the client with the characteristic's UUID. When the client unsubscribes, the row is removed. Tracking subscriptions in the user interface is optional — the framework knows the subscription state internally and does not need the application to assist it.

procedure TfmMain.wclGattServerSubscribed(Sender: TObject;
  const Client: TwclGattServerClient;
  const Characteristic: TwclGattLocalCharacteristic);
begin
  Trace('Client ' + IntToHex(Client.Address, 12) + ' subscribed to');
  Trace(4, 'Characteristic: ' + IntToHex(Characteristic.Uuid.ShortUuid, 4));

  // Client already connected on this stage.
  Item := FindItem(Client);
  if Item <> nil then begin
    Item := lvDevices.Items.Insert(Item.Index + 1);
    Item.Caption := '';
    Item.Data := Characteristic;
    Item.SubItems.Add(IntToHex(Characteristic.Uuid.ShortUuid, 4));
    // ...fill remaining columns with empty strings...

    UpdateNotificationSize(Client);
    UpdateNotifyAllStatus;
  end;
end;

procedure TfmMain.wclGattServerUnsubscribed(Sender: TObject;
  const Client: TwclGattServerClient;
  const Characteristic: TwclGattLocalCharacteristic);
begin
  Trace('Client ' + IntToHex(Client.Address, 12) + ' unsubscribed from');
  Trace(4, 'Characteristic: ' + IntToHex(Characteristic.Uuid.ShortUuid, 4));

  Item := FindItem(Client);
  if Item <> nil then begin
    if Item.Index < lvDevices.Items.Count - 1 then begin
      for i := Item.Index + 1 to lvDevices.Items.Count - 1 do begin
        Item := lvDevices.Items[i];
        if Item.Caption <> '' then
          Break;

        if Item.Data = Characteristic then begin
          lvDevices.Items.Delete(Item.Index);

          UpdateNotificationSize(Client);
          UpdateNotifyAllStatus;

          Break;
        end;
      end;
    end;
  end;
end;
void __fastcall TfmMain::wclGattServerSubscribed(TObject *Sender,
      const TwclGattServerClient *Client,
      const TwclGattLocalCharacteristic *Characteristic)
{
    Trace("Client " + IntToHex(Client->Address, 12) + " subscribed to");
    Trace(4, "Characteristic: " + IntToHex(Characteristic->Uuid.ShortUuid, 4));

    TListItem* Item = FindItem((TwclGattServerClient*)Client);
    if (Item != NULL)
    {
        Item = lvDevices->Items->Insert(Item->Index + 1);
        Item->Caption = "";
        Item->Data = (void*)Characteristic;
        Item->SubItems->Add(IntToHex(Characteristic->Uuid.ShortUuid, 4));
        // ...fill remaining columns with empty strings...

        UpdateNotificationSize((TwclGattServerClient*)Client);
        UpdateNotifyAllStatus();
    }
}

void __fastcall TfmMain::wclGattServerUnsubscribed(TObject *Sender,
      const TwclGattServerClient *Client,
      const TwclGattLocalCharacteristic *Characteristic)
{
    Trace("Client " + IntToHex(Client->Address, 12) + " unsubscribed from");
    Trace(4, "Characteristic: " + IntToHex(Characteristic->Uuid.ShortUuid, 4));

    TListItem* Item = FindItem((TwclGattServerClient*)Client);
    if (Item != NULL)
    {
        if (Item->Index < lvDevices->Items->Count - 1)
        {
            for (int i = Item->Index + 1; i < lvDevices->Items->Count; i++)
            {
                Item = lvDevices->Items->Item[i];
                if (Item->Caption != "")
                    break;

                if (Item->Data == (void*)Characteristic)
                {
                    lvDevices->Items->Delete(Item->Index);

                    UpdateNotificationSize((TwclGattServerClient*)Client);
                    UpdateNotifyAllStatus();

                    break;
                }
            }
        }
    }
}
void GattServerSubscribed(object Sender, wclGattServerClient Client, wclGattLocalCharacteristic Characteristic)
{
    Trace("Client " + Client.Address.ToString("X12") + " subscribed to");
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"));

    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        Item = lvDevices.Items.Insert(Item.Index + 1, "");
        Item.Tag = Characteristic;
        Item.SubItems.Add(Characteristic.Uuid.ShortUuid.ToString("X4"));
        // ...fill remaining columns with empty strings...

        UpdateNotificationSize(Client);
        UpdateNotifyAllStatus();
    }
}

void GattServerUnsubscribed(object Sender, wclGattServerClient Client, wclGattLocalCharacteristic Characteristic)
{
    Trace("Client " + Client.Address.ToString("X12") + " unsubscribed from");
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"));

    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        if (Item.Index < lvDevices.Items.Count - 1)
        {
            for (Int32 i = Item.Index + 1; i < lvDevices.Items.Count; i++)
            {
                Item = lvDevices.Items[i];
                if (Item.Text != "")
                    break;

                if (Item.Tag == Characteristic)
                {
                    lvDevices.Items.Remove(Item);

                    UpdateNotificationSize(Client);
                    UpdateNotifyAllStatus();

                    break;
                }
            }
        }
    }
}
Private Sub FServer_OnSubscribed(Sender As Object, Client As wclBluetooth.wclGattServerClient, Characteristic As wclBluetooth.wclGattLocalCharacteristic) Handles FServer.OnSubscribed
    Trace("Client " + Client.Address.ToString("X12") + " subscribed to")
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"))

    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        Item = lvDevices.Items.Insert(Item.Index + 1, "")
        Item.Tag = Characteristic
        Item.SubItems.Add(Characteristic.Uuid.ShortUuid.ToString("X4"))
        ' ...fill remaining columns with empty strings...

        UpdateNotificationSize(Client)
        UpdateNotifyAllStatus()
    End If
End Sub

Private Sub FServer_OnUnsubscribed(Sender As Object, Client As wclBluetooth.wclGattServerClient, Characteristic As wclBluetooth.wclGattLocalCharacteristic) Handles FServer.OnUnsubscribed
    Trace("Client " + Client.Address.ToString("X12") + " unsubscribed from")
    Trace(4, "Characteristic: " + Characteristic.Uuid.ShortUuid.ToString("X4"))

    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        If Item.Index < lvDevices.Items.Count - 1 Then
            For i As Integer = Item.Index + 1 To lvDevices.Items.Count - 1
                Item = lvDevices.Items(i)
                If Item.Text <> "" Then Exit For

                If Item.Tag Is Characteristic Then
                    lvDevices.Items.Remove(Item)

                    UpdateNotificationSize(Client)
                    UpdateNotifyAllStatus()

                    Exit For
                End If
            Next
        End If
    End If
End Sub
void CGattServerDlg::GattServerSubscribed(void* Sender, CwclGattServerClient* const Client,
    CwclGattLocalCharacteristic* const Characteristic)
{
    Trace(_T("Client ") + IntToHex(Client->Address) + _T(" subscribed to"));
    Trace(4, _T("Characteristic: ") + IntToHex(Characteristic->Uuid.ShortUuid));

    int Item = FindItem(Client);
    if (Item > -1)
    {
        Item++;
        lvDevices.InsertItem(Item, _T(""));
        lvDevices.SetItemData(Item, (DWORD_PTR)Characteristic);
        lvDevices.SetItemText(Item, 1, IntToHex(Characteristic->Uuid.ShortUuid));
        // ...fill remaining columns with empty strings...

        UpdateNotificationSize(Client);
        UpdateNotifyAllStatus();
    }
}

void CGattServerDlg::GattServerUnsubscribed(void* Sender, CwclGattServerClient* const Client,
    CwclGattLocalCharacteristic* const Characteristic)
{
    Trace(_T("Client ") + IntToHex(Client->Address) + _T(" unsubscribed from"));
    Trace(4, _T("Characteristic: ") + IntToHex(Characteristic->Uuid.ShortUuid));

    int Item = FindItem(Client);
    if (Item > -1)
    {
        if (Item < lvDevices.GetItemCount() - 1)
        {
            for (int i = Item + 1; i < lvDevices.GetItemCount(); i++)
            {
                if (lvDevices.GetItemText(i, 0) != _T(""))
                    break;

                if ((CwclGattLocalCharacteristic*)lvDevices.GetItemData(i) == Characteristic)
                {
                    lvDevices.DeleteItem(i);

                    UpdateNotificationSize(Client);
                    UpdateNotifyAllStatus();

                    break;
                }
            }
        }
    }
}

Sending notifications

Once a client has subscribed, the server can push new data to it. The Notify method of the wclGattLocalCharacteristic class is the single entry point for both notifications and indications — the framework chooses the correct PDU type based on the characteristic's properties. There are two overloads:

  • Notify(data, length) — sends the value to every client subscribed to this characteristic.
  • Notify(address, data, length) — sends the value to a single client identified by its Bluetooth address.

The GattServer sample application exposes both through two buttons. The "Notify all" button sends the current data to every subscriber and demonstrates the first overload. The "Notify" button sends the data to the single client associated with the selected list row, using the second overload. The demo also measures the elapsed time for each send, so that the cost of a notification can be observed on the user interface.

Notifications are subject to the link's maximum notification size, which the server can read on a per-client basis. If the payload exceeds the limit, the framework splits it automatically across several PDUs. Writing notification handlers that respect the caller's timing — for example, not sending more than a few notifications per connection event — is the application's responsibility.

Characteristic := TwclGattLocalCharacteristic(lvDevices.Selected.Data);

// Data conversion skipped for clarity: build a Byte array from edData.Text.

for i := 0 to Repeats - 1 do begin
  if Address = 0 then begin
    Start := GetTickCount;
    Res := Characteristic.Notify(@Data[0], DataLen);
    _End := GetTickCount;
  end else begin
    Start := GetTickCount;
    Res := Characteristic.Notify(Address, @Data[0], DataLen);
    _End := GetTickCount;
  end;

  if Res <> WCL_E_SUCCESS then
    Trace(8, 'Attempt [' + IntToStr(i + 1) + ']', Res)
  else
    Trace(8, 'Attempt [' + IntToStr(i + 1) + '] took ' +
      IntToStr(_End - Start) + ' ms');
end;
TwclGattLocalCharacteristic* Characteristic = (TwclGattLocalCharacteristic*)lvDevices->Selected->Data;

// Data conversion skipped for clarity: build a Byte array from edData.Text.

for (int i = 0; i < Repeats; i++)
{
    DWORD Start;
    DWORD End;
    int Res;
    if (Address == 0)
    {
        Start = GetTickCount();
        Res = Characteristic->Notify(&Data[0], DataLen);
        End = GetTickCount();
    }
    else
    {
        Start = GetTickCount();
        Res = Characteristic->Notify(Address, &Data[0], DataLen);
        End = GetTickCount();
    }

    if (Res != WCL_E_SUCCESS)
        Trace(8, "Attempt [" + IntToStr(i + 1) + "]", Res);
    else
        Trace(8, "Attempt [" + IntToStr(i + 1) + "] took " +
            IntToStr((int)(End - Start)) + " ms");
}
wclGattLocalCharacteristic Characteristic = (wclGattLocalCharacteristic)lvDevices.SelectedItems[0].Tag;

// Data conversion skipped for clarity: build a Byte array from edData.Text.

for (Int32 i = 0; i < Repeats; i++)
{
    Int32 Start;
    Int32 End;
    Int32 Res;
    if (Address == 0)
    {
        Start = Environment.TickCount;
        Res = Characteristic.Notify(Data);
        End = Environment.TickCount;
    }
    else
    {
        Start = Environment.TickCount;
        Res = Characteristic.Notify(Address, Data);
        End = Environment.TickCount;
    }

    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace(8, "Attempt [" + (i + 1).ToString() + "]", Res);
    else
        Trace(8, "Attempt [" + (i + 1).ToString() + "] took " +
            (End - Start).ToString() + " ms");
}
Dim Characteristic As wclGattLocalCharacteristic = CType(lvDevices.SelectedItems(0).Tag, wclGattLocalCharacteristic)

' Data conversion skipped for clarity: build a Byte array from edData.Text.

For i As Integer = 0 To Repeats - 1
    Dim Start As Int32
    Dim [End] As Int32
    Dim Res As Int32
    If Address = 0 Then
        Start = Environment.TickCount
        Res = Characteristic.Notify(Data)
        [End] = Environment.TickCount
    Else
        Start = Environment.TickCount
        Res = Characteristic.Notify(Address, Data)
        [End] = Environment.TickCount
    End If
    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace(8, "Attempt [" + (i + 1).ToString() + "]", Res)
    Else
        Trace(8, "Attempt [" + (i + 1).ToString() + "] took " + ([End] - Start).ToString() + " ms")
    End If
Next
CwclGattLocalCharacteristic* Characteristic = (CwclGattLocalCharacteristic*)lvDevices.GetItemData(Item);

// Data conversion skipped for clarity: build a Byte array from edData.Text.

for (i = 0; i < Repeats; i++)
{
    DWORD Start;
    DWORD End;
    int Res;

    if (Address == 0)
    {
        Start = GetTickCount();
        Res = Characteristic->Notify(Data, DataLen);
        End = GetTickCount();
    }
    else
    {
        Start = GetTickCount();
        Res = Characteristic->Notify(Address, Data, DataLen);
        End = GetTickCount();
    }

    if (Res != WCL_E_SUCCESS)
        Trace(8, _T("Attempt [") + IntToStr(i + 1) + _T("]"), Res);
    else
    {
        Trace(8, _T("Attempt [") + IntToStr(i + 1) + _T("] took ") +
            IntToStr(End - Start) + _T(" ms"));
    }
}

Per-client parameters

Every connected client has a set of BLE link parameters that the server can read and, in most cases, change. The wclGattServerClient object exposes those parameters through dedicated methods, and the framework fires an event whenever any of them changes so that the application does not have to poll.

Reading parameters

Four pieces of information are available on a per-client basis:

  • Maximum PDU size. The largest ATT PDU that the client and the server have agreed on for this link. Read with GetMaxPduSize. The value changes during the life of the connection whenever the peer renegotiates the MTU, which the framework reports through the OnMaxPduSizeChanged event.
  • Connection parameters. The active interval, latency, and supervision timeout of the link. Read with GetConnectionParams. See the connection parameters article for a full explanation of what each of the three values means and how they affect throughput and power consumption.
  • Connection PHY. The active physical layer in each direction, in the form of four flags per direction: Coded, Uncoded1MPhy, Uncoded2MPhy. Read with GetConnectionPhyInfo. The BLE PHY article describes the underlying PHY types. This information is only available on Windows 11 and above.
  • Maximum notification size. The largest value the client is willing to receive in a single notification PDU. Read with GetMaxNotificationSize. This value is renegotiated when the client changes its subscription preferences, and the framework reports the change through the OnNotificationSizeChanged event.
procedure TfmMain.UpdateMaxPduSize(const Client: TwclGattServerClient);
var
  Item: TListItem;
  Size: Word;
begin
  Item := FindItem(Client);
  if Item <> nil then begin
    if Client.GetMaxPduSize(Size) = WCL_E_SUCCESS then
      Item.SubItems[1] := IntToStr(Size)
    else
      Item.SubItems[1] := '-----';
  end;
end;

procedure TfmMain.UpdateConnectionParams(const Client: TwclGattServerClient);
var
  Item: TListItem;
  Params: TwclBluetoothLeConnectionParameters;
begin
  Item := FindItem(Client);
  if Item <> nil then begin
    if Client.GetConnectionParams(Params) = WCL_E_SUCCESS then begin
      Item.SubItems[2] := IntToStr(Params.Interval);
      Item.SubItems[3] := IntToStr(Params.Latency);
      Item.SubItems[4] := IntToStr(Params.LinkTimeout);

    end else begin
      Item.SubItems[2] := '-----';
      Item.SubItems[3] := '-----';
      Item.SubItems[4] := '-----';
    end;
  end;
end;

procedure TfmMain.UpdatePhy(const Client: TwclGattServerClient);
var
  Item: TListItem;
  Phy: TwclBluetoothLeConnectionPhy;
begin
  Item := FindItem(Client);
  if Item <> nil then begin
    if Client.GetConnectionPhyInfo(Phy) = WCL_E_SUCCESS then begin
      Item.SubItems[5] := BoolToStr(Phy.Receive.IsCoded);
      Item.SubItems[6] := BoolToStr(Phy.Receive.IsUncoded1MPhy);
      Item.SubItems[7] := BoolToStr(Phy.Receive.IsUncoded2MPhy);

      Item.SubItems[8] := BoolToStr(Phy.Transmit.IsCoded);
      Item.SubItems[9] := BoolToStr(Phy.Transmit.IsUncoded1MPhy);
      Item.SubItems[10] := BoolToStr(Phy.Transmit.IsUncoded2MPhy);

    end else begin
      // ...fill columns 5 through 10 with "-----"...
    end;
  end;
end;

procedure TfmMain.UpdateNotificationSize(const Client: TwclGattServerClient);
var
  Item: TListItem;
  Size: Word;
begin
  Item := FindItem(Client);
  if Item <> nil then begin
    if Client.GetMaxNotificationSize(Size) = WCL_E_SUCCESS then
      Item.SubItems[11] := IntToStr(Size)
    else
      Item.SubItems[11] := '-----';
  end;
end;
void __fastcall TfmMain::UpdateMaxPduSize(TwclGattServerClient* const Client)
{
    TListItem* Item = FindItem(Client);
    if (Item != NULL)
    {
        Word Size;
        if (Client->GetMaxPduSize(Size) == WCL_E_SUCCESS)
            Item->SubItems->Strings[1] = IntToStr(Size);
        else
            Item->SubItems->Strings[1] = "-----";
    }
}

void __fastcall TfmMain::UpdateConnectionParams(
        TwclGattServerClient* const Client)
{
    TListItem* Item = FindItem(Client);
    if (Item != NULL)
    {
        TwclBluetoothLeConnectionParameters Params;
        if (Client->GetConnectionParams(Params) == WCL_E_SUCCESS)
        {
            Item->SubItems->Strings[2] = IntToStr(Params.Interval);
            Item->SubItems->Strings[3] = IntToStr(Params.Latency);
            Item->SubItems->Strings[4] = IntToStr(Params.LinkTimeout);
        }
        else
        {
            Item->SubItems->Strings[2] = "-----";
            Item->SubItems->Strings[3] = "-----";
            Item->SubItems->Strings[4] = "-----";
        }
    }
}

void __fastcall TfmMain::UpdatePhy(TwclGattServerClient* const Client)
{
    TListItem* Item = FindItem(Client);
    if (Item != NULL)
    {
        TwclBluetoothLeConnectionPhy Phy;
        if (Client->GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
        {
            Item->SubItems->Strings[5] = BoolToStr(Phy.Receive.IsCoded);
            Item->SubItems->Strings[6] = BoolToStr(Phy.Receive.IsUncoded1MPhy);
            Item->SubItems->Strings[7] = BoolToStr(Phy.Receive.IsUncoded2MPhy);

            Item->SubItems->Strings[8] = BoolToStr(Phy.Transmit.IsCoded);
            Item->SubItems->Strings[9] = BoolToStr(Phy.Transmit.IsUncoded1MPhy);
            Item->SubItems->Strings[10] = BoolToStr(Phy.Transmit.IsUncoded2MPhy);
        }
        else
        {
            // ...fill columns 5 through 10 with "-----"...
        }
    }
}

void __fastcall TfmMain::UpdateNotificationSize(
        TwclGattServerClient* const Client)
{
    TListItem* Item = FindItem(Client);
    if (Item != NULL)
    {
        Word Size;
        if (Client->GetMaxNotificationSize(Size) == WCL_E_SUCCESS)
            Item->SubItems->Strings[11] = IntToStr(Size);
        else
            Item->SubItems->Strings[11] = "-----";
    }
}
private void UpdateMaxPduSize(wclGattServerClient Client)
{
    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        UInt16 Size;
        if (Client.GetMaxPduSize(out Size) == wclErrors.WCL_E_SUCCESS)
            Item.SubItems[2].Text = Size.ToString();
        else
            Item.SubItems[2].Text = "-----";
    }
}

private void UpdateConnectionParams(wclGattServerClient Client)
{
    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        wclBluetoothLeConnectionParameters Params;
        if (Client.GetConnectionParams(out Params) == wclErrors.WCL_E_SUCCESS)
        {
            Item.SubItems[3].Text = Params.Interval.ToString();
            Item.SubItems[4].Text = Params.Latency.ToString();
            Item.SubItems[5].Text = Params.LinkTimeout.ToString();
        }
        else
        {
            Item.SubItems[3].Text = "-----";
            Item.SubItems[4].Text = "-----";
            Item.SubItems[5].Text = "-----";
        }
    }
}

private void UpdatePhy(wclGattServerClient Client)
{
    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        wclBluetoothLeConnectionPhy Phy;
        if (Client.GetConnectionPhyInfo(out Phy) == wclErrors.WCL_E_SUCCESS)
        {
            Item.SubItems[6].Text = Phy.Receive.IsCoded.ToString();
            Item.SubItems[7].Text = Phy.Receive.IsUncoded1MPhy.ToString();
            Item.SubItems[8].Text = Phy.Receive.IsUncoded2MPhy.ToString();

            Item.SubItems[9].Text = Phy.Transmit.IsCoded.ToString();
            Item.SubItems[10].Text = Phy.Transmit.IsUncoded1MPhy.ToString();
            Item.SubItems[11].Text = Phy.Transmit.IsUncoded2MPhy.ToString();
        }
        else
        {
            // ...fill columns 6 through 11 with "-----"...
        }
    }
}

private void UpdateNotificationSize(wclGattServerClient Client)
{
    ListViewItem Item = FindItem(Client);
    if (Item != null)
    {
        UInt16 Size;
        if (Client.GetMaxNotificationSize(out Size) == wclErrors.WCL_E_SUCCESS)
            Item.SubItems[12].Text = Size.ToString();
        else
            Item.SubItems[12].Text = "-----";
    }
}
Private Sub UpdateMaxPduSize(Client As wclGattServerClient)
    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        Dim Size As UInt16
        If Client.GetMaxPduSize(Size) = wclErrors.WCL_E_SUCCESS Then
            Item.SubItems(2).Text = Size.ToString()
        Else
            Item.SubItems(2).Text = "-----"
        End If
    End If
End Sub

Private Sub UpdateConnectionParams(Client As wclGattServerClient)
    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        Dim Params As wclBluetoothLeConnectionParameters
        If Client.GetConnectionParams(Params) = wclErrors.WCL_E_SUCCESS Then
            Item.SubItems(3).Text = Params.Interval.ToString()
            Item.SubItems(4).Text = Params.Latency.ToString()
            Item.SubItems(5).Text = Params.LinkTimeout.ToString()
        Else
            Item.SubItems(3).Text = "-----"
            Item.SubItems(4).Text = "-----"
            Item.SubItems(5).Text = "-----"
        End If
    End If
End Sub

Private Sub UpdatePhy(Client As wclGattServerClient)
    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        Dim Phy As wclBluetoothLeConnectionPhy
        If Client.GetConnectionPhyInfo(Phy) = wclErrors.WCL_E_SUCCESS Then
            Item.SubItems(6).Text = Phy.Receive.IsCoded.ToString()
            Item.SubItems(7).Text = Phy.Receive.IsUncoded1MPhy.ToString()
            Item.SubItems(8).Text = Phy.Receive.IsUncoded2MPhy.ToString()

            Item.SubItems(9).Text = Phy.Transmit.IsCoded.ToString()
            Item.SubItems(10).Text = Phy.Transmit.IsUncoded1MPhy.ToString()
            Item.SubItems(11).Text = Phy.Transmit.IsUncoded2MPhy.ToString()
        Else
            ' ...fill columns 6 through 11 with "-----"...
        End If
    End If
End Sub

Private Sub UpdateNotificationSize(Client As wclGattServerClient)
    Dim Item As ListViewItem = FindItem(Client)
    If Item IsNot Nothing Then
        Dim Size As UInt16
        If Client.GetMaxNotificationSize(Size) = wclErrors.WCL_E_SUCCESS Then
            Item.SubItems(12).Text = Size.ToString()
        Else
            Item.SubItems(12).Text = "-----"
        End If
    End If
End Sub
void CGattServerDlg::UpdateMaxPduSize(CwclGattServerClient* const Client)
{
    int Item = FindItem(Client);
    if (Item > -1)
    {
        unsigned short Size;
        if (Client->GetMaxPduSize(Size) == WCL_E_SUCCESS)
            lvDevices.SetItemText(Item, 2, IntToStr(Size));
        else
            lvDevices.SetItemText(Item, 2, _T("-----"));
    }
}

void CGattServerDlg::UpdateConnectionParams(CwclGattServerClient* const Client)
{
    int Item = FindItem(Client);
    if (Item > -1)
    {
        wclBluetoothLeConnectionParameters Params;
        if (Client->GetConnectionParams(Params) == WCL_E_SUCCESS)
        {
            lvDevices.SetItemText(Item, 3, IntToStr(Params.Interval));
            lvDevices.SetItemText(Item, 4, IntToStr(Params.Latency));
            lvDevices.SetItemText(Item, 5, IntToStr(Params.LinkTimeout));
        }
        else
        {
            lvDevices.SetItemText(Item, 3, _T("-----"));
            lvDevices.SetItemText(Item, 4, _T("-----"));
            lvDevices.SetItemText(Item, 5, _T("-----"));
        }
    }
}

void CGattServerDlg::UpdatePhy(CwclGattServerClient* const Client)
{
    int Item = FindItem(Client);
    if (Item > -1)
    {
        wclBluetoothLeConnectionPhy Phy;
        if (Client->GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
        {
            lvDevices.SetItemText(Item, 6, BoolToStr(Phy.Receive.IsCoded));
            lvDevices.SetItemText(Item, 7, BoolToStr(Phy.Receive.IsUncoded1MPhy));
            lvDevices.SetItemText(Item, 8, BoolToStr(Phy.Receive.IsUncoded2MPhy));

            lvDevices.SetItemText(Item, 9, BoolToStr(Phy.Transmit.IsCoded));
            lvDevices.SetItemText(Item, 10, BoolToStr(Phy.Transmit.IsUncoded1MPhy));
            lvDevices.SetItemText(Item, 11, BoolToStr(Phy.Transmit.IsUncoded2MPhy));
        }
        else
        {
            // ...fill columns 6 through 11 with "-----"...
        }
    }
}

void CGattServerDlg::UpdateNotificationSize(CwclGattServerClient* const Client)
{
    int Item = FindItem(Client);
    if (Item > -1)
    {
        unsigned short Size;
        if (Client->GetMaxNotificationSize(Size) == WCL_E_SUCCESS)
            lvDevices.SetItemText(Item, 12, IntToStr(Size));
        else
            lvDevices.SetItemText(Item, 12, _T("-----"));
    }
}

Setting connection parameters

The server can request a change to the connection parameters of an already-connected client. The client is not required to accept the request, but in practice most centrals honour it. The SetConnectionParams method of the wclGattServerClient class has two overloads:

  • Predefined profile. A wclBluetoothLeConnectionParametersType value: ppBalanced, ppPowerOptimized, or ppThroughputOptimized. The exact values applied by each profile are chosen by the operating system and reflect the system's policy for that use case.
  • Custom values. A wclBluetoothLeConnectionParametersValue structure with explicit MinInterval, MaxInterval, Latency, and LinkTimeout fields. The sample uses 84, 84, 0, and 800 respectively. The interval fields are in units of 1.25 ms, the latency field is a count of events, and the timeout field is in units of 10 ms.

Requesting a change does not guarantee that the peer will accept it — both sides negotiate the final set, and the framework reports the outcome through the OnConnectionParamsChanged event. If the new values are successfully negotiated, that event fires with the updated parameters, which the sample writes back into the client row.

Client := TwclGattServerClient(lvDevices.Selected.Data);

Trace('Set connection parameters for ' + IntToHex(Client.Address, 12));
if cbParams.ItemIndex = 3 then begin
  ParamsValue.MinInterval := 84;
  ParamsValue.MaxInterval := 84;
  ParamsValue.Latency := 0;
  ParamsValue.LinkTimeout := 800;
  Res := Client.SetConnectionParams(ParamsValue);

end else begin
  case cbParams.ItemIndex of
    0: Params := ppBalanced;
    1: Params := ppPowerOptimized;
    2: Params := ppThroughputOptimized;
    else raise wclEInvalidArgument.Create('Invalid connection parameters.');
  end;
  Res := Client.SetConnectionParams(Params);
end;

if Res <> WCL_E_SUCCESS then
  Trace(4, 'Set connection parameters', Res)
else
  Trace(4, 'Connection parameters set');
TwclGattServerClient* Client = (TwclGattServerClient*)lvDevices->Selected->Data;

Trace("Set connection parameters for " + IntToHex(Client->Address, 12));
int Res;
if (cbParams->ItemIndex == 3)
{
    TwclBluetoothLeConnectionParametersValue ParamsValue;
    ParamsValue.MinInterval = 84;
    ParamsValue.MaxInterval = 84;
    ParamsValue.Latency = 0;
    ParamsValue.LinkTimeout = 800;
    Res = Client->SetConnectionParams(ParamsValue);
}
else
{
    TwclBluetoothLeConnectionParametersType Params;
    switch (cbParams->ItemIndex)
    {
        case 0:
            Params = ppBalanced;
            break;
        case 1:
            Params = ppPowerOptimized;
            break;
        case 2:
            Params = ppThroughputOptimized;
            break;
        default:
            throw new wclEInvalidArgument("Invalid connection parameters.");
    }
    Res = Client->SetConnectionParams(Params);
}

if (Res != WCL_E_SUCCESS)
    Trace(4, "Set connection parameters", Res);
else
    Trace(4, "Connection parameters set");
wclGattServerClient Client = (wclGattServerClient)lvDevices.SelectedItems[0].Tag;

Trace("Set connection parameters for " + Client.Address.ToString("X12"));

Int32 Res;
wclBluetoothLeConnectionParametersValue ParamsValue = new wclBluetoothLeConnectionParametersValue();
wclBluetoothLeConnectionParametersType Params;
if (cbParams.SelectedIndex == 3)
{
    ParamsValue.MinInterval = 84;
    ParamsValue.MaxInterval = 84;
    ParamsValue.Latency = 0;
    ParamsValue.LinkTimeout = 800;
    Res = Client.SetConnectionParams(ParamsValue);
}
else
{
    switch (cbParams.SelectedIndex)
    {
        case 0:
            Params = wclBluetoothLeConnectionParametersType.ppBalanced;
            break;
        case 1:
            Params = wclBluetoothLeConnectionParametersType.ppPowerOptimized;
            break;
        case 2:
            Params = wclBluetoothLeConnectionParametersType.ppThroughputOptimized;
            break;
        default:
            throw new wclEInvalidArgument("Invalid connection parameters.");
    }
    Res = Client.SetConnectionParams(Params);
}

if (Res != wclErrors.WCL_E_SUCCESS)
    Trace(4, "Set connection parameters", Res);
else
    Trace(4, "Connection parameters set");
Dim Client As wclGattServerClient = CType(lvDevices.SelectedItems(0).Tag, wclGattServerClient)

Trace("Set connection parameters for " + Client.Address.ToString("X12"))

Dim Res As Int32
Dim ParamsValue As wclBluetoothLeConnectionParametersValue = New wclBluetoothLeConnectionParametersValue()
Dim Params As wclBluetoothLeConnectionParametersType
If cbParams.SelectedIndex = 3 Then
    ParamsValue.MinInterval = 84
    ParamsValue.MaxInterval = 84
    ParamsValue.Latency = 0
    ParamsValue.LinkTimeout = 800
    Res = Client.SetConnectionParams(ParamsValue)
Else
    Select cbParams.SelectedIndex
        Case 0
            Params = wclBluetoothLeConnectionParametersType.ppBalanced
        Case 1
            Params = wclBluetoothLeConnectionParametersType.ppPowerOptimized
        Case 2
            Params = wclBluetoothLeConnectionParametersType.ppThroughputOptimized
        Case Else
            Throw New wclEInvalidArgument("Invalid connection parameters.")
    End Select
    Res = Client.SetConnectionParams(Params)
End If

If Res <> wclErrors.WCL_E_SUCCESS Then
    Trace(4, "Set connection parameters", Res)
Else
    Trace(4, "Connection parameters set")
End If
CwclGattServerClient* Client = (CwclGattServerClient*)lvDevices.GetItemData(Item);

Trace(_T("Set connection parameters for ") + IntToHex(Client->Address));

int Res;
if (cbParams.GetCurSel() == 3)
{
    wclBluetoothLeConnectionParametersValue ParamsValue;
    ParamsValue.MinInterval = 84;
    ParamsValue.MaxInterval = 84;
    ParamsValue.Latency = 0;
    ParamsValue.LinkTimeout = 800;
    Res = Client->SetConnectionParams(ParamsValue);
}
else
{
    wclBluetoothLeConnectionParametersType Params;
    switch (cbParams.GetCurSel())
    {
        case 0:
            Params = ppBalanced;
            break;
        case 1:
            Params = ppPowerOptimized;
            break;
        case 2:
            Params = ppThroughputOptimized;
            break;
        default:
            throw new wclEInvalidArgument("Invalid connection parameters.");
    }
    Res = Client->SetConnectionParams(Params);
}

if (Res != WCL_E_SUCCESS)
    Trace(4, _T("Set connection parameters"), Res);
else
    Trace(4, _T("Connection parameters set"));

Stopping the server

Stopping the GATT server reverses the startup sequence. The Stop method stops advertising and disconnects any remaining clients; Uninitialize releases the service and characteristic objects; Close on the Bluetooth manager releases the radio. The OnStopped event fires when the server has finished shutting down, and the BeforeClose and OnClosed events of the Bluetooth manager mark the same transitions on the manager side.

The same teardown logic runs when the application exits — the sample calls Stop from its FormDestroy handler and unhooks all events before releasing the objects. Applications that use wclBluetoothManager in service or console applications should follow the same pattern, since leaving a server running when the process exits can leave a stale GATT service visible to nearby centrals until the radio is reset. See the message processing article for how the events are delivered in non-UI applications.

procedure TfmMain.Stop;
var
  Res: Integer;
begin
  Trace('Stopping GATT server');

  Res := wclGattServer.Stop;
  if Res <> WCL_E_SUCCESS then
    Trace('Stop GATT server', Res);

  UninitializeServer;
  UninitializeBluetooth;
end;

procedure TfmMain.FormDestroy(Sender: TObject);
begin
  Stop;
end;
void __fastcall TfmMain::Stop()
{
    Trace("Stopping GATT server");

    int Res = wclGattServer->Stop();
    if (Res != WCL_E_SUCCESS)
        Trace("Stop GATT server", Res);

    UninitializeServer();
    UninitializeBluetooth();
}

void __fastcall TfmMain::FormDestroy(TObject *Sender)
{
    Stop();
}
private void Stop()
{
    Trace("Stopping GATT server");

    Int32 Res = FServer.Stop();
    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace("Stop GATT server", Res);

    UninitializeServer();
    UninitializeBluetooth();
}

private void fmMain_FormClosed(object sender, FormClosedEventArgs e)
{
    Stop();
}
Private Sub [Stop]()
    Trace("Stopping GATT server")

    Dim Res As Int32 = FServer.Stop()
    If Res <> wclErrors.WCL_E_SUCCESS Then Trace("Stop GATT server", Res)

    UninitializeServer()
    UninitializeBluetooth()
End Sub

Private Sub fmMain_FormClosed(sender As System.Object, e As System.Windows.Forms.FormClosedEventArgs) Handles MyBase.FormClosed
    [Stop]()
End Sub
void CGattServerDlg::Stop()
{
    Trace(_T("Stopping GATT server"));

    int Res = FServer.Stop();
    if (Res != WCL_E_SUCCESS)
        Trace(_T("Stop GATT server"), Res);

    UninitializeServer();
    UninitializeBluetooth();
}

void CGattServerDlg::OnDestroy()
{
    CDialog::OnDestroy();

    Stop();

    __unhook(&FServer);
    __unhook(&FManager);
}

Frequently asked questions

What is a GATT server in Bluetooth Low Energy?
A GATT server (peripheral) holds the GATT database, controls access to it, and provides its services, characteristics, and descriptors to connected GATT clients (centrals). It answers read and write requests and can push notifications and indications to subscribed clients. See the GATT client article for the central side of the same protocol.
How do I start a GATT server with the Bluetooth Framework?
Use the wclGattServer class. Open the Bluetooth manager, get an LE radio, call Initialize, add a service and characteristics, then call Start. The server begins advertising automatically and fires OnClientConnected when a central connects.
How can I detect a client connection on the GATT server side?
The Bluetooth Framework delivers real client connection and disconnection events through OnClientConnected and OnClientDisconnected. You do not need to wait for a client to read or subscribe to a characteristic to know that it is connected. See GATT connection state change detection for details.