A BLE transmitter broadcasting advertisement packets to multiple receivers

Bluetooth Low Energy allows devices to broadcast information without establishing a connection. Such broadcasts are known as Bluetooth LE Advertisement. They are used by Bluetooth Beacons, Apple Continuity, Drone Remote ID, and many other applications. Advertising is also part of the discovery process and carries information about nearby Bluetooth-enabled devices.

The Bluetooth Framework provides methods to receive and decode BLE advertisement packets, also known as frames, as well as to build and broadcast custom advertisements. The library package includes the Beacons sample application that shows both sides of this workflow.


Advertisement and connection

Bluetooth LE advertising is a connectionless broadcast. A transmitter sends advertisement packets at a fixed interval, and any receiver in radio range can pick them up without pairing, bonding, or establishing a connection. This is the fundamental difference between advertising and a GATT connection: advertising is one-to-many, GATT is one-to-one and requires an established link.

Not every advertisement is the same. The Bluetooth specification defines several advertisement packet types, and the Bluetooth Framework reports them through the PacketType parameter of the information frame that the watcher delivers (see Beacon Watcher events below):

  • Connectable undirected — any receiver may initiate a connection. Used by devices that want to be discovered and paired, not by typical beacons.
  • Connectable directed — only a specific receiver is allowed to connect. Used during reconnection to a known peer.
  • Scannable undirected — not connectable, but a receiver may send a scan request and receive a scan response that carries additional data. The scan response gives the advertiser a second packet's worth of payload.
  • Non-connectable undirected — pure broadcast, no connection, no scan response. This is what a typical beacon uses.
  • Scan response — the reply to a scan request. It is delivered as a separate frame and is not an advertisement on its own.
  • Extended — an extended advertisement (Bluetooth 5). See Bluetooth 5 Extended Advertising.

Why does this matter for beacons? Because a beacon is usually non-connectable by design. Its job is to broadcast an identifier or a payload, not to accept connections. A connectable advertisement ties up the radio in a way that prevents it from rotating through multiple formats, and it exposes the device to unsolicited connection attempts. The Bluetooth Framework's advertiser always transmits non-connectable advertising — the packet types above are informational, delivered to the watcher for observers that want to distinguish a connectable device from a beacon.

Because advertising is connectionless, the receiver never acknowledges a packet. There is no retransmission, no flow control, and no guarantee that any given advertisement was received. A beacon that needs reliability has to broadcast repeatedly and accept that some fraction of packets will be lost to interference, distance, or channel conditions.


Receive BLE Advertisements

Beacon watcher receiving BLE advertisement frames and decoding them into structured data

To receive Bluetooth LE advertisements with the Bluetooth Framework, use the wclBluetoothLeBeaconWatcher class. It monitors nearby BLE devices and decodes their advertisement frames. The class can decode the following advertisement types:

  • Appearance
  • Basic Information
  • Extended Information
  • 16-bit, 32-bit, and 128-bit Service Data
  • 16-bit, 32-bit, and 128-bit Solicitation Service
  • TX Power
  • 128-bit UUID
  • AltBeacon
  • Eddystone TLM, UID, and URL
  • Apple Continuity
  • Microsoft CDP Beacon
  • Drone Remote ID
  • Manufacturer Raw

Unknown frames are passed to the application as a raw byte stream. The stream can be decoded by the application if needed.

Preparing the manager and radio

Every operation in the Bluetooth Framework starts with a working radio. The wclBluetoothManager class enumerates the radios available on the system, opens the Bluetooth driver, and hands out a wclBluetoothRadio object. Both the beacon watcher and the LE advertiser take a radio as the argument to their Start method — without one, nothing happens.

The standard pattern is to open the manager lazily, on first use, and then ask it for a working LE radio. The GetLeRadio method returns the first radio that supports Bluetooth Low Energy, which is what beacons need. If the system has no LE-capable radio, the method returns an error and the application should report it rather than silently failing.

The snippet below is the complete implementation from the Beacons sample.

function TfmMain.GetRadio: TwclBluetoothRadio;
var
  Res: Integer;
begin
  Result := nil;

  if not BluetoothManager.Active then
    Res := BluetoothManager.Open
  else
    Res := WCL_E_SUCCESS;

  if Res <> WCL_E_SUCCESS then
    Trace('Bluetooth Manager open', Res)

  else begin
    Res := BluetoothManager.GetLeRadio(Result);
    if Res <> WCL_E_SUCCESS then
      Trace('Get working radio', Res)
    else
      Trace('Use ' + Result.ApiName + ' radio');
  end;
end;
TwclBluetoothRadio* __fastcall TfmMain::GetRadio()
{
        int Res = WCL_E_SUCCESS;
        if (!BluetoothManager->Active)
                Res = BluetoothManager->Open();

        if (Res != WCL_E_SUCCESS)
        {
                Trace("Bluetooth Manager open", Res);
                return NULL;
        }

        TwclBluetoothRadio* Radio;
        Res = BluetoothManager->GetLeRadio(Radio);
        if (Res != WCL_E_SUCCESS)
        {
                Trace("Get working radio", Res);
                return NULL;
        }

        Trace("Use " + Radio->ApiName + " radio");
        return Radio;
}
private wclBluetoothRadio GetRadio()
{
    Int32 Res = wclErrors.WCL_E_SUCCESS;
    if (!BluetoothManager.Active)
        Res = BluetoothManager.Open();

    if (Res != wclErrors.WCL_E_SUCCESS)
    {
        Trace("Bluetooth Manager open", Res);
        return null;
    }

    wclBluetoothRadio Radio;
    Res = BluetoothManager.GetLeRadio(out Radio);
    if (Res != wclErrors.WCL_E_SUCCESS)
    {
        Trace("Get working radio", Res);
        return null;
    }

    Trace("Use " + Radio.ApiName + " radio");
    return Radio;
}
Private Function GetRadio() As wclBluetoothRadio
    Dim Res As Int32 = wclErrors.WCL_E_SUCCESS
    If Not BluetoothManager.Active Then Res = BluetoothManager.Open()

    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace("Bluetooth Manager open", Res)
        Return Nothing
    End If

    Dim Radio As wclBluetoothRadio = Nothing
    Res = BluetoothManager.GetLeRadio(Radio)
    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace("Get working radio", Res)
        Return Nothing
    End If

    Trace("Use " + Radio.ApiName + " radio")
    Return Radio
End Function
CwclBluetoothRadio* CBeaconsDlg::GetRadio()
{
	int Res = WCL_E_SUCCESS;
	if (!BluetoothManager.Active)
		Res = BluetoothManager.Open();

	if (Res != WCL_E_SUCCESS)
	{
		Trace(_T("Bluetooth Manager open"), Res);
		return NULL;
	}

	CwclBluetoothRadio* Radio;
	Res = BluetoothManager.GetLeRadio(Radio);
	if (Res != WCL_E_SUCCESS)
	{
		Trace(_T("Get working radio"), Res);
		return NULL;
	}

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

Three things to note about this function:

  • The manager is opened only once. The Active property reports whether it is already open, so the second and subsequent calls skip the Open step. The BluetoothManagerAfterOpen event fires on the first call only.
  • The returned radio is owned by the manager. The application must not free it; it becomes invalid when the manager is closed.
  • The manager must be closed when the application exits. In the Beacons sample this happens in the form's destruction handler — FormDestroy in Delphi and C++ Builder, fmMainFormClosed in C# and VB.NET, OnDestroy in the native C++ sample.

Starting the Beacon Watcher

Before capturing advertisements, obtain a working radio from the wclBluetoothManager, configure the watcher's scanning mode, and hook the frame events. The snippet below shows the minimal start and stop logic along with an example event handler.

procedure TfmMain.btWatcherStartClick(Sender: TObject);
var
  Radio: TwclBluetoothRadio;
  Res: Integer;
begin
  Radio := GetRadio;
  if Radio <> nil then begin
    BeaconWatcher.ScanningMode := smActive;
    BeaconWatcher.AllowExtendedAdvertisements := True;
    Res := BeaconWatcher.Start(Radio);
    if Res <> WCL_E_SUCCESS then
      Trace('Start watcher failed', Res);
  end;
end;

procedure TfmMain.btWatcherStopClick(Sender: TObject);
var
  Res: Integer;
begin
  Res := BeaconWatcher.Stop;
  if Res <> WCL_E_SUCCESS then
    Trace('Stop watcher failed', Res);
end;

procedure TfmMain.BeaconWatcherAdvertisementFrameInformation(
  Sender: TObject; const Address: Int64; const Timestamp: Int64;
  const Rssi: SByte; const Name: string;
  const PacketType: TwclBluetoothLeAdvertisementType;
  const Flags: TwclBluetoothLeAdvertisementFlags);
begin
  Log('Frame from ' + IntToHex(Address, 12) +
    ' Name: ' + Name + ' RSSI: ' + IntToStr(Rssi));
end;
void __fastcall TfmMain::btWatcherStartClick(TObject *Sender)
{
    TwclBluetoothRadio* Radio = GetRadio();
    if (Radio != NULL)
    {
        BeaconWatcher->ScanningMode = smActive;
        BeaconWatcher->AllowExtendedAdvertisements = true;
        int Res = BeaconWatcher->Start(Radio);
        if (Res != WCL_E_SUCCESS)
            Trace("Start watcher failed", Res);
    }
}

void __fastcall TfmMain::btWatcherStopClick(TObject *Sender)
{
    int Res = BeaconWatcher->Stop();
    if (Res != WCL_E_SUCCESS)
        Trace("Stop watcher failed", Res);
}

void __fastcall TfmMain::BeaconWatcherAdvertisementFrameInformation(
    TObject *Sender, const __int64 Address, const __int64 Timestamp,
    const SByte Rssi, const String Name,
    const TwclBluetoothLeAdvertisementType PacketType,
    const TwclBluetoothLeAdvertisementFlags Flags)
{
    Log("Frame from " + IntToHex(Address, 12) +
        " Name: " + Name + " RSSI: " + IntToStr(Rssi));
}
private void btWatcherStart_Click(Object sender, EventArgs e)
{
    wclBluetoothRadio Radio = GetRadio();
    if (Radio != null)
    {
        BeaconWatcher.ScanningMode = wclBluetoothLeScanningMode.smActive;
        BeaconWatcher.AllowExtendedAdvertisements = true;
        Int32 Res = BeaconWatcher.Start(Radio);
        if (Res != wclErrors.WCL_E_SUCCESS)
            Trace("Start watcher failed", Res);
    }
}

private void btWatcherStop_Click(Object sender, EventArgs e)
{
    Int32 Res = BeaconWatcher.Stop();
    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace("Stop watcher failed", Res);
}

void BeaconWatcherAdvertisementFrameInformation(Object Sender,
    long Address, long Timestamp, sbyte Rssi, string Name,
    wclBluetoothLeAdvertisementType PacketType,
    wclBluetoothLeAdvertisementFlag Flags)
{
    Log("Frame from " + Address.ToString("X12") +
        " Name: " + Name + " RSSI: " + Rssi.ToString());
}
Private Sub btWatcherStart_Click(sender As Object, e As EventArgs)
        Handles btWatcherStart.Click
    Dim Radio As wclBluetoothRadio = GetRadio()
    If Radio IsNot Nothing Then
        BeaconWatcher.ScanningMode = wclBluetoothLeScanningMode.smActive
        BeaconWatcher.AllowExtendedAdvertisements = True
        Dim Res As Int32 = BeaconWatcher.Start(Radio)
        If Res <> wclErrors.WCL_E_SUCCESS Then
            Trace("Start watcher failed", Res)
        End If
    End If
End Sub

Private Sub btWatcherStop_Click(sender As Object, e As EventArgs)
        Handles btWatcherStop.Click
    Dim Res As Int32 = BeaconWatcher.Stop()
    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace("Stop watcher failed", Res)
    End If
End Sub

Private Sub BeaconWatcherAdvertisementFrameInformation(
        Sender As Object, Address As Long, Timestamp As Long,
        Rssi As SByte, Name As String,
        PacketType As wclBluetoothLeAdvertisementType,
        Flags As wclBluetoothLeAdvertisementFlag)
        Handles BeaconWatcher.OnAdvertisementFrameInformation

    Log("Frame from " & Address.ToString("X12") &
        " Name: " & Name & " RSSI: " & Rssi.ToString())
End Sub
void CBeaconsDlg::OnBnClickedButtonWatcherStart()
{
    CwclBluetoothRadio* Radio = GetRadio();
    if (Radio != NULL)
    {
        BeaconWatcher.ScanningMode = smActive;
        BeaconWatcher.AllowExtendedAdvertisements = true;
        int Res = BeaconWatcher.Start(Radio);
        if (Res != WCL_E_SUCCESS)
            Trace(_T("Start watcher failed"), Res);
    }
}

void CBeaconsDlg::OnBnClickedButtonWatcherStop()
{
    int Res = BeaconWatcher.Stop();
    if (Res != WCL_E_SUCCESS)
        Trace(_T("Stop watcher failed"), Res);
}

CwclBluetoothRadio* CBeaconsDlg::GetRadio()
{
    if (!BluetoothManager.Active)
        BluetoothManager.Open();
    CwclBluetoothRadio* Radio = NULL;
    BluetoothManager.GetLeRadio(Radio);
    return Radio;
}

void CBeaconsDlg::BeaconWatcherAdvertisementFrameInformation(
    void* Sender, const __int64 Address, const __int64 Timestamp,
    const char Rssi, const tstring& Name,
    const wclBluetoothLeAdvertisementType PacketType,
    const wclBluetoothLeAdvertisementFlags& Flags)
{
    UNREFERENCED_PARAMETER(Sender);
    // Store or display the frame data
}

Scanning modes

Two scanning modes are available: active and passive. The difference is what the watcher does after it receives a connectable or scannable advertisement.

  • Active (smActive) — after receiving an advertisement, the watcher sends a scan request. The advertiser may reply with a scan response that carries additional data. Legacy advertising uses the scan response to deliver payload that does not fit in the main packet, so most beacon receivers use active mode.
  • Passive (smPassive) — the watcher only listens. It never sends a scan request and therefore never receives a scan response. This is slightly cheaper on the radio and is the right choice when the receiver is itself battery-powered, or when the application only cares about the main advertisement.

For beacon reception, active mode is almost always the correct choice — most beacon formats carry their full payload in the advertisement packet itself, so passive scanning works too, but active mode is safer because it also picks up the scan response if the transmitter sends one. Passive mode is a deliberate optimisation for receivers that must preserve their own battery.

A related set of scan parameters controls the scan window and scan interval. The framework exposes them as the ScanInterval and ScanWindow properties, with ScanParametersType selecting a preset: ptDefault, ptCoexistenceOptimized, ptLowLatency, or ptCustom. The Beacons sample uses the presets and only enables the two edit fields when the type is ptCustom.

Beacon Watcher events

The watcher does not deliver every frame through a single event. Instead, it exposes a small map of events, and the application subscribes only to the ones it cares about. The events fall into four groups:

Generic information. OnAdvertisementFrameInformation fires once per advertisement and delivers the address, timestamp, RSSI, local name, packet type, and advertisement flags. It is the event most applications start with, because it is enough to log every nearby device even when none of the specialised decoders apply.

Per-format events. Each supported beacon format has its own event: OnAppleiBeaconFrame, OnAltBeaconFrame, OnEddystoneUidFrame, OnEddystoneUrlFrame, OnEddystoneTlmFrame, OnManufacturerRawFrame, OnMicrosoftCdpBeaconFrame, and a set of Apple Continuity events (OnAppleAirdropFrame, OnAppleAirplaySourceFrame, OnAppleAirplayTargetFrame, OnAppleAirprintFrame, OnAppleHomeKitFrame, OnAppleNearbyActionFrame, OnAppleNearbyInfoFrame, OnAppleProximityPairingFrame, OnAppleUnknownFrame, OnAppleFindMyFrame, OnAppleHandoffFrame, OnAppleHeySiriFrame, OnAppleMagicSwitchFrame, OnAppleTetheringSourceFrame, OnAppleTetheringTargetFrame). Each event delivers a strongly-typed payload, so the application does not need to parse the bytes itself. The exact fields delivered by each format, together with the corresponding class in the Beacons sample, are described in Bluetooth LE Beacon Formats.

Raw fallbacks. Three events cover frames that do not fit a specific format: OnAdvertisementRawFrame delivers individual AD structures that the framework did not decode, together with the data type byte; OnAdvertisementReceived delivers the entire advertisement as a single byte stream; and OnManufacturerRawFrame delivers any manufacturer-specific frame together with its company identifier. The last one is listed among the per-format events above, but functionally it is the manufacturer-specific fallback.

Extended advertising. OnAdvertisementExtFrameInformation fires for Bluetooth 5 extended advertisements and carries the fields that only the extended format exposes: address type, TX power, extended frame flags, and the primary and secondary PHY. The details of this event and the properties that produce it are covered in Bluetooth 5 Extended Advertising.

An application typically subscribes to OnAdvertisementFrameInformation for a raw log, plus one or two per-format events for the specific decoders it needs. Subscribing to all of them is fine, but each event fires separately, so the handler code tends to dominate the application's size — the Beacons sample subscribes to all of them because its job is to display every frame it can.


Transmit BLE Advertising

BLE advertiser broadcasting custom advertisement packets to nearby receivers

The Bluetooth Framework also allows transmitting custom BLE advertisements. The wclBluetoothLeAdvertiser class switches the application to Bluetooth LE Advertiser mode. It supports predefined advertisements as well as fully custom packets, subject to the restrictions described below.

Reserved Advertisement Types

The following advertisement types are reserved and not allowed:

TypeConstantDescription
00LE_GAP_AD_TYPE_NONEEmpty flags
01LE_GAP_AD_TYPE_FLAGSFlags
08LE_GAP_AD_TYPE_LOCAL_NAME_SHORTShortened Local Name
09LE_GAP_AD_TYPE_LOCAL_NAME_COMPLETEComplete Local Name
1BLE_GAP_AD_TYPE_ADDRESSLE Bluetooth Device Address
1CLE_GAP_AD_TYPE_ROLELE Role

The following types are additionally reserved by the Microsoft Bluetooth driver and not allowed when the advertiser runs on top of it:

TypeConstantDescription
02LE_GAP_AD_TYPE_SERVICES_16_MOREIncomplete List of 16-bit Service UUIDs
03LE_GAP_AD_TYPE_SERVICES_16_ALLComplete List of 16-bit Service UUIDs
04LE_GAP_AD_TYPE_SERVICES_32_MOREIncomplete List of 32-bit Service UUIDs
05LE_GAP_AD_TYPE_SERVICES_32_ALLComplete List of 32-bit Service UUIDs
06LE_GAP_AD_TYPE_SERVICES_128_MOREIncomplete List of 128-bit Service UUIDs
07LE_GAP_AD_TYPE_SERVICES_128_ALLComplete List of 128-bit Service UUIDs
0ALE_GAP_AD_TYPE_TXPOWERTx Power Level
0DLE_GAP_AD_TYPE_CODClass of Device
0ELE_GAP_AD_TYPE_PAIRING_HASH_CSimple Pairing Hash C192
0FLE_GAP_AD_TYPE_PAIRING_RAND_RSimple Pairing Randomizer R192
10LE_GAP_AD_TYPE_DEVICE_IDSecurity Manager TK Values
11LE_GAP_AD_TYPE_SM_OOB_FLAGSSecurity Manager OOB Flags
12LE_GAP_AD_TYPE_CON_INTERVALSSlave Connection Interval Range
17LE_GAP_AD_TYPE_PUBLIC_TARGET_ADDRESSPublic Target Address
18LE_GAP_AD_TYPE_RANDOM_TARGET_ADDRESSRandom Target Address
19LE_GAP_AD_TYPE_APPEARANCEAppearance
1ALE_GAP_AD_TYPE_ADV_INTERVALAdvertising Interval
1DLE_GAP_AD_TYPE_PAIRING_HASH_C_256Simple Pairing Hash C256
1ELE_GAP_AD_TYPE_PAIRING_RAND_R_256Simple Pairing Randomizer R256
3DLE_GAP_AD_TYPE_3D_INFO3D Information Data

Extended Advertisement

If the hardware supports Bluetooth 5 and above, the Bluetooth Framework can use the Extended Advertisement Format when running with the Microsoft Bluetooth driver on Windows 10 and above. Enable it by setting the UseExtendedAdvertisement property of the wclBluetoothLeAdvertiser class. When the property is set to true, three additional properties control the advertisement:

  • Anonymous — when set to true, the device address is not included in the advertisement header.
  • IncludeTxRssi — when set to true, the transmit power level is included in the advertisement header.
  • PreferredTxRssi — requests the radio to use the indicated transmit power level. To disable, set the value to -127.

If the application enables Extended Advertisement on hardware or an OS that does not support it, the WCL_E_BLUETOOTH_LE_EXT_ADV_NOT_SUPPORTED error code is returned when the advertisement is started.

Advertiser lifecycle

The advertiser holds a list of advertisement objects, not a single advertisement. The workflow is always the same four steps: Clear the previous list, Add one or more advertisement objects, Start the advertiser with a working radio, and later Stop it. Between Start and Stop the radio broadcasts the advertisements in rotation; after Stop the list is preserved, so a second Start resumes the same rotation without re-adding the objects.

The Add method returns a result code. The application is expected to check it and, in native code, free the object it passed in if the call failed. The Beacons sample wraps this in a small helper, because the same error handling is repeated for every advertisement type:

procedure TfmMain.AddAdvertisement(const Adv: TwclBluetoothLeAdvertisement);
var
  Res: Integer;
begin
  Res := LeAdvertiser.Add(Adv);
  if Res <> WCL_E_SUCCESS then begin
    Trace('Add advertisement', Res);
    Adv.Free;
  end;
end;
void __fastcall TfmMain::AddAdvertisement(TwclBluetoothLeAdvertisement* const Adv)
{
        int Res = LeAdvertiser->Add(Adv);
        if (Res != WCL_E_SUCCESS)
        {
                Trace("Add advertisement", Res);
                Adv->Free();
        }
}
private void AddAdvertisement(wclBluetoothLeAdvertisement Adv)
{
    Int32 Res = LeAdvertiser.Add(Adv);
    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace("Add advertisement", Res);
}
Private Sub AddAdvertisement(Adv As wclBluetoothLeAdvertisement)
    Dim Res As Int32 = LeAdvertiser.Add(Adv)
    If Res <> wclErrors.WCL_E_SUCCESS Then Trace("Add advertisement", Res)
End Sub
void CBeaconsDlg::AddAdvertisement(CwclBluetoothLeAdvertisement* const Adv)
{
	int Res = LeAdvertiser.Add(Adv);
	if (Res != WCL_E_SUCCESS)
	{
		Trace(_T("Add advertisement"), Res);
		delete Adv;
	}
}

Two ownership rules follow from this pattern. First, the list is owned by the advertiser — calling Clear releases every object that was previously added, so the application must not reuse or free those objects after Clear. Second, if Add returns an error, the advertiser has not taken ownership of the object, so the caller is responsible for freeing it (as the Delphi, C++ Builder, and native C++ samples do). The managed code samples (C#, VB.NET) rely on the garbage collector for the failure path, which is why their helpers only log the error.

Because the list survives a Stop, the application can stop and restart the advertiser without rebuilding the advertisement list. Rebuilding is only necessary when the content of the advertisements changes — for example, when the user edits the beacon UUID, or when a different format is selected in the UI.

Interval and rotation

The Interval property sets how often the advertiser transmits. Lower values make the device more discoverable and reduce the latency of telemetry updates, but they increase power consumption and the amount of airtime the radio uses. Higher values do the opposite: they save power and reduce congestion, at the cost of slower discovery and coarser telemetry granularity. The Beacons sample exposes the interval as an editable field and passes the parsed value directly to the property.

When more than one advertisement is added to the same advertiser, the interval applies to the rotation as a whole rather than to each advertisement individually. The advertiser transmits one advertisement per interval, then moves to the next one in the list, and so on, cycling. The effective rate of any single advertisement is therefore the configured interval divided by the number of advertisements in the list. This is what makes it possible to broadcast iBeacon to legacy scanners and Eddystone URL to browsers from a single radio, at the cost of broadcasting each of them less often.

The interval is a hint to the underlying driver, not a guarantee. The driver may round the value to the nearest supported step, and the radio itself may not start a new advertisement while it is still transmitting the previous one. Applications that need precise timing should read back the value after the advertiser starts, or rely on the interval only as a relative ordering between different configurations.

Starting the LE Advertiser

The wclBluetoothLeAdvertiser class allows broadcasting standard beacon frames such as iBeacon, Eddystone, and AltBeacon, as well as fully custom raw advertisements. When more than one advertisement is added, the advertiser does not transmit them simultaneously — it rotates through them, sending one advertisement, then the next, then the next, and so on, cycling. The example below creates an iBeacon and a manufacturer-specific advertisement, adds both to the advertiser's rotation, enables extended advertising, and starts the transmission.

const
  BEACON_UUID: TGUID = '{09039835-4A80-443B-87AA-DC565D09EA61}';

procedure TfmMain.btAdvertiserStartClick(Sender: TObject);
var
  Radio: TwclBluetoothRadio;
  Adv: TwclBluetoothLeAdvertisement;
  Data: TwclBluetoothLeAdvertisementFrameRawData;
  Res: Integer;
begin
  Radio := GetRadio;
  if Radio = nil then
    Exit;

  LeAdvertiser.Clear;

  // iBeacon
  Adv := TwclBluetoothLeiBeaconAdvertisement.Create(
    -5, $0101, $0202, BEACON_UUID);
  LeAdvertiser.Add(Adv);

  // Manufacturer specific
  SetLength(Data, 2);
  Data[0] := $12;
  Data[1] := $34;
  Adv := TwclBluetoothLeManufacturerAdvertisement.Create($010E, Data);
  LeAdvertiser.Add(Adv);

  // Extended advertisement (Bluetooth 5)
  LeAdvertiser.UseExtendedAdvertisement := True;
  LeAdvertiser.Anonymous := False;
  LeAdvertiser.IncludeTxRssi := True;
  LeAdvertiser.PreferredTxRssi := 10;
  LeAdvertiser.Interval := 100;

  Res := LeAdvertiser.Start(Radio);
  if Res <> WCL_E_SUCCESS then
    Trace('Start advertiser failed', Res);
end;

procedure TfmMain.btAdvertiserStopClick(Sender: TObject);
var
  Res: Integer;
begin
  Res := LeAdvertiser.Stop;
  if Res <> WCL_E_SUCCESS then
    Trace('Stop advertiser failed', Res);
end;
const GUID BEACON_UUID = {
    0x09039835, 0x4A80, 0x443B,
    { 0x87, 0xAA, 0xDC, 0x56, 0x5D, 0x09, 0xEA, 0x61 }
};

void __fastcall TfmMain::btAdvertiserStartClick(TObject *Sender)
{
    TwclBluetoothRadio* Radio = GetRadio();
    if (Radio == NULL)
        return;

    LeAdvertiser->Clear();

    // iBeacon
    TwclBluetoothLeiBeaconAdvertisement* Adv;
    Adv = new TwclBluetoothLeiBeaconAdvertisement(
        -5, 0x0101, 0x0202, BEACON_UUID);
    LeAdvertiser->Add(Adv);

    // Manufacturer specific
    TwclBluetoothLeAdvertisementFrameRawData Data;
    Data.Length = 2;
    Data[0] = 0x12;
    Data[1] = 0x34;
    Adv = new TwclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    LeAdvertiser->Add(Adv);

    // Extended advertisement
    LeAdvertiser->UseExtendedAdvertisement = true;
    LeAdvertiser->Anonymous = false;
    LeAdvertiser->IncludeTxRssi = true;
    LeAdvertiser->PreferredTxRssi = 10;
    LeAdvertiser->Interval = 100;

    int Res = LeAdvertiser->Start(Radio);
    if (Res != WCL_E_SUCCESS)
        Trace("Start advertiser failed", Res);
}

void __fastcall TfmMain::btAdvertiserStopClick(TObject *Sender)
{
    int Res = LeAdvertiser->Stop();
    if (Res != WCL_E_SUCCESS)
        Trace("Stop advertiser failed", Res);
}
private static readonly Guid BEACON_UUID =
    new Guid("{09039835-4A80-443B-87AA-DC565D09EA61}");

private void btAdvertiserStart_Click(Object sender, EventArgs e)
{
    wclBluetoothRadio Radio = GetRadio();
    if (Radio == null)
        return;

    LeAdvertiser.Clear();

    // iBeacon
    wclBluetoothLeAdvertisement Adv =
        new wclBluetoothLeiBeaconAdvertisement(
            -5, 0x0101, 0x0202, BEACON_UUID);
    LeAdvertiser.Add(Adv);

    // Manufacturer specific
    byte[] Data = new byte[2] { 0x12, 0x34 };
    Adv = new wclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    LeAdvertiser.Add(Adv);

    // Extended advertisement
    LeAdvertiser.UseExtendedAdvertisement = true;
    LeAdvertiser.Anonymous = false;
    LeAdvertiser.IncludeTxRssi = true;
    LeAdvertiser.PreferredTxRssi = 10;
    LeAdvertiser.Interval = 100;

    Int32 Res = LeAdvertiser.Start(Radio);
    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace("Start advertiser failed", Res);
}

private void btAdvertiserStop_Click(Object sender, EventArgs e)
{
    Int32 Res = LeAdvertiser.Stop();
    if (Res != wclErrors.WCL_E_SUCCESS)
        Trace("Stop advertiser failed", Res);
}
Private Shared ReadOnly BEACON_UUID As New Guid(
    "{09039835-4A80-443B-87AA-DC565D09EA61}")

Private Sub btAdvertiserStart_Click(sender As Object, e As EventArgs)
        Handles btAdvertiserStart.Click
    Dim Radio As wclBluetoothRadio = GetRadio()
    If Radio Is Nothing Then Return

    LeAdvertiser.Clear()

    ' iBeacon
    Dim Adv As wclBluetoothLeAdvertisement =
        New wclBluetoothLeiBeaconAdvertisement(
            -5, &H101, &H202, BEACON_UUID)
    LeAdvertiser.Add(Adv)

    ' Manufacturer specific
    Dim Data As Byte() = New Byte() {&H12, &H34}
    Adv = New wclBluetoothLeManufacturerAdvertisement(&H10E, Data)
    LeAdvertiser.Add(Adv)

    ' Extended advertisement
    LeAdvertiser.UseExtendedAdvertisement = True
    LeAdvertiser.Anonymous = False
    LeAdvertiser.IncludeTxRssi = True
    LeAdvertiser.PreferredTxRssi = 10
    LeAdvertiser.Interval = 100

    Dim Res As Int32 = LeAdvertiser.Start(Radio)
    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace("Start advertiser failed", Res)
    End If
End Sub

Private Sub btAdvertiserStop_Click(sender As Object, e As EventArgs)
        Handles btAdvertiserStop.Click
    Dim Res As Int32 = LeAdvertiser.Stop()
    If Res <> wclErrors.WCL_E_SUCCESS Then
        Trace("Stop advertiser failed", Res)
    End If
End Sub
static const GUID BEACON_UUID = {
    0x09039835, 0x4A80, 0x443B,
    { 0x87, 0xAA, 0xDC, 0x56, 0x5D, 0x09, 0xEA, 0x61 }
};

void CBeaconsDlg::OnBnClickedButtonAdvertiserStart()
{
    CwclBluetoothRadio* Radio = GetRadio();
    if (Radio == NULL)
        return;

    LeAdvertiser.Clear();

    // iBeacon
    CwclBluetoothLeAdvertisement* Adv;
    Adv = new CwclBluetoothLeiBeaconAdvertisement(
        -5, 0x0101, 0x0202, BEACON_UUID);
    LeAdvertiser.Add(Adv);

    // Manufacturer specific
    wclBluetoothLeAdvertisementFrameRawData Data;
    Data.resize(2);
    Data[0] = 0x12;
    Data[1] = 0x34;
    Adv = new CwclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    LeAdvertiser.Add(Adv);

    // Extended advertisement
    LeAdvertiser.UseExtendedAdvertisement = true;
    LeAdvertiser.Anonymous = false;
    LeAdvertiser.IncludeTxRssi = true;
    LeAdvertiser.PreferredTxRssi = 10;
    LeAdvertiser.Interval = 100;

    int Res = LeAdvertiser.Start(Radio);
    if (Res != WCL_E_SUCCESS)
        Trace(_T("Start advertiser failed"), Res);
}

void CBeaconsDlg::OnBnClickedButtonAdvertiserStop()
{
    int Res = LeAdvertiser.Stop();
    if (Res != WCL_E_SUCCESS)
        Trace(_T("Stop advertiser failed"), Res);
}

Frequently asked questions

What is Bluetooth LE Advertisement?
It is the broadcast transmission of data by BLE devices without establishing a connection, used for beacons, identification, and telemetry.
How to receive BLE packets with Bluetooth Framework?
Use the wclBluetoothLeBeaconWatcher class. It decodes dozens of standard packet types, including Eddystone, Apple Continuity, and Drone Remote ID.
Can I transmit custom BLE advertising packets?
Yes. The wclBluetoothLeAdvertiser class supports both predefined and fully custom advertisement packets, subject to driver restrictions. Multiple advertisements can be added to the same advertiser, but they are broadcast in rotation — not simultaneously.
Which scanning mode should I use — active or passive?
Active scanning (smActive) also sends scan requests and picks up scan responses, which is the safer default for beacon receivers. Passive scanning (smPassive) only listens, uses slightly less radio time, and is appropriate when the receiver is battery-powered or when scan-response payloads are not needed.
What does the advertising Interval control?
The Interval property sets how often the advertiser transmits. Short intervals (for example, 100 ms) reduce discovery latency but consume more power and airtime; long intervals (for example, 1000 ms) do the opposite. When several advertisements are added to the same advertiser, the interval applies to the rotation as a whole — one advertisement is sent per interval, then the advertiser moves to the next.
Do I need to open the Bluetooth manager manually?
Yes. The wclBluetoothManager must be opened before any radio can be used, and closed when the application exits. A common pattern is to open it lazily on first use and close it in the form's destruction handler.