Comparison of iBeacon, AltBeacon, Eddystone UID, Eddystone URL, Eddystone TLM, and manufacturer-specific BLE advertisement payload layouts

Every Bluetooth LE beacon is, at the radio level, exactly the same thing: an advertising packet broadcast at regular intervals. What makes iBeacon different from Eddystone or from a manufacturer-specific frame is only the content of the AD structure inside that packet. The format is a convention, not a technology.

That convention matters because it defines what a receiver can do with the payload. A shared format means every receiver knows how to decode the fields — a UUID, a URL, a battery level — without any prior coordination. A proprietary format means the receiver must be told what to expect, but in return it can carry arbitrary structured data.

The Bluetooth Framework implements native support for the most common formats on both sides — creation and parsing — and additionally exposes a raw byte-level interface for anything the standard formats do not cover. This article walks through each of them in turn, then compares them, then shows how to transmit and receive them.


What a beacon really is

Underneath the branding, a beacon is an advertising packet. BLE devices broadcast these packets on channels 37, 38, and 39 at a configurable interval — usually between 100 ms and 1 s for beacon use cases. Any device in radio range can receive the packet without pairing, without connecting, and without any prior knowledge of the transmitter.

The packet itself is a sequence of AD structures. Each structure is a length byte, a type byte, and a payload. The type byte tells the receiver how to interpret the payload — 0x01 for flags, 0x09 for the complete local name, 0xFF for manufacturer-specific data, and so on. Beacon formats are conventions for what that payload contains.

Because the payload is limited, a legacy advertisement packet carries at most 31 bytes. Beacon formats are designed to fit inside that limit, which is why iBeacon uses a 16-byte UUID plus two 16-bit numbers, and why Eddystone URL uses a compressed URL encoding. Extended Advertising lifts that ceiling but is only available on Bluetooth 5 hardware.


Beacon formats

Six formats are widely deployed today. They are not mutually exclusive — a single radio can transmit several of them simultaneously — and each was designed for a specific set of priorities.

iBeacon

Apple's iBeacon is the oldest and the most widely deployed format. It carries three identifiers and a transmitter power reference:

  • UUID — a 128-bit identifier that represents the beacon operator or the deployment.
  • Major — a 16-bit number that groups beacons inside a deployment, for example one per building.
  • Minor — a 16-bit number that identifies the individual beacon, for example one per room.
  • TX Power — a signed byte with the measured RSSI at one metre from the beacon. Receivers use it to estimate distance.

The identifiers are fixed and meaningless to the transmitter — an iBeacon just broadcasts them. It is up to the receiving application to map a UUID/Major/Minor triplet to an actual meaning, typically by querying a backend. This is deliberate: the beacon operator can change what a beacon means without touching the hardware.

AltBeacon

AltBeacon was created by Radius Networks as an openly licensed alternative to iBeacon. The format is structurally similar — a beacon ID followed by a reference RSSI — but is explicitly not tied to any vendor's ecosystem and does not require a licence to use.

The beacon ID is 20 bytes instead of iBeacon's 20-byte equivalent (16+2+2). It is usually interpreted as UUID + Major + Minor, matching iBeacon in practice, but the specification does not require that interpretation. A manufacturer ID and a reserved byte complete the payload. AltBeacon's main advantage is legal: it can be deployed without any concern about Apple's licensing terms for iBeacon.

Eddystone UID

Google's Eddystone is not a single format but a family of four. The UID frame is the closest analogue to iBeacon. It carries a 10-byte Namespace and a 6-byte Instance, plus a transmitter power reference byte. Both identifiers are application-defined and are normally mapped to a meaning through a backend service.

Compared to iBeacon, Eddystone UID is more compact: 17 bytes of payload versus iBeacon's 21. This leaves more room in the packet for other AD structures, which matters when a device wants to broadcast more than one thing at once.

Eddystone URL

Eddystone URL is the format that made physical web experiments practical. Instead of an opaque identifier, the payload contains an actual URL, compressed to fit inside a 31-byte packet. A URL like https://www.btframework.com fits into roughly 12 bytes of payload after compression.

The compression scheme uses a one-byte scheme prefix (0x00 for http://www., 0x01 for https://www., 0x02 for http://, 0x03 for https://) and a table of one-byte substitutions for common top-level domains. Receivers reverse the encoding and present the URL to the user. The Bluetooth Framework returns the fully decoded URL string directly.

Eddystone TLM

Eddystone TLM is not a beacon format in the usual sense — it does not carry an identifier, and it is not meant to be scanned by end-user applications. It carries telemetry about the transmitter itself:

  • Battery voltage — a 16-bit value in millivolts.
  • Temperature — a signed 16-bit value in units of 1/256 °C.
  • Advertisement count — a 32-bit counter of advertisements sent since boot.
  • Seconds since boot — a 32-bit counter that allows the receiver to compute a rough estimate of how long the beacon has been running.

TLM frames are usually interleaved with UID or URL frames on the same beacon — for example, every tenth advertisement is a TLM frame and the rest are UID frames. This is how operators monitor a deployed fleet without visiting the physical devices.

Microsoft CDP

The Microsoft Connected Devices Platform (CDP) uses BLE advertisements for proximity-based features like Near Share. The advertisement carries a scenario type, a device type, a hash derived from the current user's identity, a salt value, and several status bits. The protocol is not intended for third-party use as a beacon format, but the Bluetooth Framework can decode it because it appears in the wild whenever Windows devices are nearby.

Applications that decode CDP frames typically use them for presence detection — distinguishing an Android phone from an iPhone from a Windows laptop without any cooperation from the user — rather than for beacon-style identification.


Custom and manufacturer-specific frames

Standard beacon formats cover the common cases, but they impose structure. When an application needs to broadcast something that does not fit iBeacon or Eddystone — a proprietary telemetry payload, a device-specific state machine, or a proprietary protocol that predates the framework — the raw advertisement interface takes over.

There are two distinct ways to broadcast a custom payload, and the difference matters.

Manufacturer-specific data

Manufacturer-specific data is a standard BLE concept. Its AD type is 0xFF, and its payload always begins with a 16-bit Company Identifier assigned by the Bluetooth SIG. The rest of the payload is defined by the company that owns the identifier. Any receiver that sees this advertisement knows immediately which vendor's convention applies.

The wclBluetoothLeManufacturerAdvertisement class models this. It takes a company identifier and a byte array:

if cbAdvertiserManufacturer.Checked then begin
  Data := nil;
  SetLength(Data, 2);
  Data[0] := $12;
  Data[1] := $34;
  Adv := TwclBluetoothLeManufacturerAdvertisement.Create($010E, Data);
  AddAdvertisement(Adv);
end;
// Create and add manufacturer specific advertisement.
if (cbAdvertiserManufacturer.GetCheck())
{
    Data.resize(2);
    Data[0] = 0x12;
    Data[1] = 0x34;
    Adv = new CwclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    AddAdvertisement(Adv);
}
if (cbAdvertiserManufacturer.Checked)
{
    Data = new Byte[2];
    Data[0] = 0x12;
    Data[1] = 0x34;
    Adv = new wclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    AddAdvertisement(Adv);
}
If cbAdvertiserManufacturer.Checked Then
    ReDim Data(2)
    Data(0) = &H12
    Data(1) = &H34
    Adv = New wclBluetoothLeManufacturerAdvertisement(&H10E, Data)
    AddAdvertisement(Adv)
End If
// Create and add manufacturer specific advertisement.
if (cbAdvertiserManufacturer.GetCheck())
{
    Data.resize(2);
    Data[0] = 0x12;
    Data[1] = 0x34;
    Adv = new CwclBluetoothLeManufacturerAdvertisement(0x010E, Data);
    AddAdvertisement(Adv);
}

The 0x010E in the example is a placeholder company identifier. Real deployments use the identifier assigned to their organisation by the Bluetooth SIG. Any value in the range that is not assigned to another company is technically allowed but will confuse receivers that expect the format to follow a known convention.

Fully custom raw frame

A raw advertisement goes further. It does not assume any AD type, any company identifier, and any payload structure. The application supplies the complete byte sequence, including the length byte and the AD type byte of each structure. The wclBluetoothLeRawAdvertisement class is the escape hatch for anything that does not fit a defined format:

if cbAdvertiserCustom.Checked then begin
  Data := nil;
  SetLength(Data, 20);

  // 16 bit Service Data
  Data[0] := $09;                            // Length.
  Data[1] := LE_GAP_AD_TYPE_SERVICE_DATA_16; // Data type.
  Data[2] := $12;                            // 16 UUID LO BYTE
  Data[3] := $34;                            // 16 UUID HI BYTE
  Data[4] := $FF;                            // Data 1st byte
  Data[5] := $FE;                            // Data 2st byte
  Data[6] := $FD;                            // Data 3st byte
  Data[7] := $FC;                            // Data 4st byte
  Data[8] := $FB;                            // Data 5st byte
  Data[9] := $FA;                            // Data 6st byte
  // Manufacturer specific data.
  Data[10] := $09;                           // Length.
  Data[11] := LE_GAP_AD_TYPE_MANUFACTURER;   // Data type.
  Data[12] := $10;                           // Company ID.
  Data[13] := $12;                           // Company ID.
  Data[14] := $01;                           // Data 1st byte
  Data[15] := $02;                           // Data 2st byte
  Data[16] := $03;                           // Data 3st byte
  Data[17] := $04;                           // Data 4st byte
  Data[18] := $05;                           // Data 5st byte
  Data[19] := $06;                           // Data 6st byte

  Adv := TwclBluetoothLeRawAdvertisement.Create(Data);
  AddAdvertisement(Adv);
end;
if (cbAdvertiserCustom.GetCheck())
{
    Data.resize(20);

    // 16 bit Service Data
    Data[0] = 0x09;                            // Length.
    Data[1] = LE_GAP_AD_TYPE_SERVICE_DATA_16;  // Data type.
    Data[2] = 0x12;                            // 16 UUID LO BYTE
    Data[3] = 0x34;                            // 16 UUID HI BYTE
    Data[4] = 0xFF;                            // Data 1st byte
    Data[5] = 0xFE;                            // Data 2st byte
    Data[6] = 0xFD;                            // Data 3st byte
    Data[7] = 0xFC;                            // Data 4st byte
    Data[8] = 0xFB;                            // Data 5st byte
    Data[9] = 0xFA;                            // Data 6st byte
    // Manufacturer specific data.
    Data[10] = 0x09;                           // Length.
    Data[11] = LE_GAP_AD_TYPE_MANUFACTURER;    // Data type.
    Data[12] = 0x10;                           // Company ID.
    Data[13] = 0x12;                           // Company ID.
    Data[14] = 0x01;                           // Data 1st byte
    Data[15] = 0x02;                           // Data 2st byte
    Data[16] = 0x03;                           // Data 3st byte
    Data[17] = 0x04;                           // Data 4st byte
    Data[18] = 0x05;                           // Data 5st byte
    Data[19] = 0x06;                           // Data 6st byte

    Adv = new CwclBluetoothLeRawAdvertisement(Data);
    AddAdvertisement(Adv);
}
if (cbAdvertiserCustom.Checked)
{
    Data = new Byte[20];

    // 16 bit Service Data
    Data[0] = 0x09;                            // Length.
    Data[1] = wclUUIDs.LE_GAP_AD_TYPE_SERVICE_DATA_16; // Data type.
    Data[2] = 0x12;                            // 16 UUID LO BYTE
    Data[3] = 0x34;                            // 16 UUID HI BYTE
    Data[4] = 0xFF;                            // Data 1st byte
    Data[5] = 0xFE;                            // Data 2st byte
    Data[6] = 0xFD;                            // Data 3st byte
    Data[7] = 0xFC;                            // Data 4st byte
    Data[8] = 0xFB;                            // Data 5st byte
    Data[9] = 0xFA;                            // Data 6st byte
    // Manufacturer specific data.
    Data[10] = 0x09;                           // Length.
    Data[11] = wclUUIDs.LE_GAP_AD_TYPE_MANUFACTURER; // Data type.
    Data[12] = 0x10;                           // Company ID.
    Data[13] = 0x12;                           // Company ID.
    Data[14] = 0x01;                           // Data 1st byte
    Data[15] = 0x02;                           // Data 2st byte
    Data[16] = 0x03;                           // Data 3st byte
    Data[17] = 0x04;                           // Data 4st byte
    Data[18] = 0x05;                           // Data 5st byte
    Data[19] = 0x06;                           // Data 6st byte

    Adv = new wclBluetoothLeRawAdvertisement(Data);
    AddAdvertisement(Adv);
}
If cbAdvertiserCustom.Checked Then
    ReDim Data(20)

    ' 16 bit Service Data
    Data(0) = &H9                              ' Length.
    Data(1) = wclUUIDs.LE_GAP_AD_TYPE_SERVICE_DATA_16 ' Data type.
    Data(2) = &H12                             ' 16 UUID LO BYTE
    Data(3) = &H34                             ' 16 UUID HI BYTE
    Data(4) = &HFF                             ' Data 1st byte
    Data(5) = &HFE                             ' Data 2st byte
    Data(6) = &HFD                             ' Data 3st byte
    Data(7) = &HFC                             ' Data 4st byte
    Data(8) = &HFB                             ' Data 5st byte
    Data(9) = &HFA                             ' Data 6st byte
    ' Manufacturer specific data.
    Data(10) = &H9                             ' Length.
    Data(11) = wclUUIDs.LE_GAP_AD_TYPE_MANUFACTURER ' Data type.
    Data(12) = &H10                            ' Company ID.
    Data(13) = &H12                            ' Company ID.
    Data(14) = &H1                             ' Data 1st byte
    Data(15) = &H2                             ' Data 2st byte
    Data(16) = &H3                             ' Data 3st byte
    Data(17) = &H4                             ' Data 4st byte
    Data(18) = &H5                             ' Data 5st byte
    Data(19) = &H6                             ' Data 6st byte

    Adv = New wclBluetoothLeRawAdvertisement(Data)
    AddAdvertisement(Adv)
End If
if (cbAdvertiserCustom.GetCheck())
{
    Data.resize(20);

    // 16 bit Service Data
    Data[0] = 0x09;                            // Length.
    Data[1] = LE_GAP_AD_TYPE_SERVICE_DATA_16;  // Data type.
    Data[2] = 0x12;                            // 16 UUID LO BYTE
    Data[3] = 0x34;                            // 16 UUID HI BYTE
    Data[4] = 0xFF;                            // Data 1st byte
    Data[5] = 0xFE;                            // Data 2st byte
    Data[6] = 0xFD;                            // Data 3st byte
    Data[7] = 0xFC;                            // Data 4st byte
    Data[8] = 0xFB;                            // Data 5st byte
    Data[9] = 0xFA;                            // Data 6st byte
    // Manufacturer specific data.
    Data[10] = 0x09;                           // Length.
    Data[11] = LE_GAP_AD_TYPE_MANUFACTURER;    // Data type.
    Data[12] = 0x10;                           // Company ID.
    Data[13] = 0x12;                           // Company ID.
    Data[14] = 0x01;                           // Data 1st byte
    Data[15] = 0x02;                           // Data 2st byte
    Data[16] = 0x03;                           // Data 3st byte
    Data[17] = 0x04;                           // Data 4st byte
    Data[18] = 0x05;                           // Data 5st byte
    Data[19] = 0x06;                           // Data 6st byte

    Adv = new CwclBluetoothLeRawAdvertisement(Data);
    AddAdvertisement(Adv);
}

The example constructs two standard AD structures packed into a single byte array: a 16-bit Service Data structure and a Manufacturer-specific Data structure. The framework sends the bytes verbatim. The receiver sees exactly what the application wrote — nothing is added, nothing is interpreted.

This is the most powerful option and also the most dangerous one. A raw frame bypasses every check the framework would normally perform on the payload structure. If the length byte does not match the actual data length, the packet will be malformed, and receivers will either reject it or misparse it. A raw frame is a tool for applications that have a fully specified wire format, not for general use.

When to use which

  • Manufacturer-specific data — the payload is application-defined, but it lives inside a well-defined AD structure with a company identifier. Receivers know where to find it and who it belongs to.
  • Raw frame — the application needs full control over the entire byte sequence, including multiple AD structures or non-standard AD types.
  • Standard beacon format — the payload matches an existing convention (iBeacon, Eddystone, AltBeacon) and interoperability with a wider ecosystem is the goal.

Comparison table

Format Payload Carries Licence Best for
iBeacon ~30 B UUID + Major + Minor + TX Apple (royalty-free in practice) Widest compatibility, iOS-first deployments
AltBeacon ~26 B Beacon ID + Ref RSSI + Manufacturer ID Open Open-spec deployments, iBeacon alternative
Eddystone UID ~28 B Namespace + Instance + TX Open Compact identifier, cross-platform
Eddystone URL variable Encoded URL + TX Open Direct web-link broadcast, no app required
Eddystone TLM ~24 B Battery + Temperature + Counters Open Remote fleet monitoring, interleaved with UID/URL
Microsoft CDP variable Scenario, device type, hash, salt Microsoft Presence detection of Windows / Apple / Android devices
Manufacturer data variable Company ID + application payload Proprietary Proprietary protocols with a registered company ID
Raw frame variable Fully application-defined AD structures N/A Fully custom wire formats, non-standard AD types

Choosing a format

The right format depends on what the receiver is expected to do with the advertisement, and who else has to understand it.

  • If the receiver is a native mobile app you wrote — any of the standard formats will work. Pick the one that carries the fields you need. Eddystone UID is the most compact. iBeacon is the most familiar to mobile developers.
  • If the receiver is a web browser or an operating system feature — Eddystone URL is the only format that produces a clickable link without a dedicated application on the receiving device. This is its entire reason to exist.
  • If the deployment has many beacons that need remote monitoring — Eddystone UID or URL interleaved with Eddystone TLM. The TLM frames give you battery and uptime without visiting the devices.
  • If the format is a contract with a third party — use the format that the third party expects, even if it is less compact or older. Interoperability beats elegance.
  • If the payload does not fit any existing convention — manufacturer-specific data, or a raw frame if even the AD type must be under your control.

It is also worth remembering that a single radio can transmit several formats at once. Nothing prevents a device from broadcasting iBeacon to legacy scanners, Eddystone URL to browsers, and a manufacturer-specific payload to a proprietary fleet-management tool, all in the same advertising interval. The Combining formats section below shows how.


Transmitting beacons

All beacon transmission goes through the wclBluetoothLeAdvertiser class. The workflow is always the same three steps: clear any previous content with Clear, add one or more advertisement objects with Add, then call Start with a working radio.

Standard beacon classes

Each standard format has a dedicated class. The Beacons sample creates all of them in a single advertising session, gated by user-selected checkboxes:

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

begin
  Radio := GetRadio;
  if Radio <> nil then begin
    LeAdvertiser.Clear;

    // iBeacon.
    if cbAdvertiserBeacon.Checked then begin
      Adv := TwclBluetoothLeiBeaconAdvertisement.Create(-5, $0101, $0202,
        BEACON_UUID);
      AddAdvertisement(Adv);
    end;

    // AltBeacon.
    if cbAdvertiserAltBeacon.Checked then begin
      Adv := TwclBluetoothLeAltBeaconAdvertisement.Create(-5, $0101, $0202,
        BEACON_UUID, $FFFE, $11);
      AddAdvertisement(Adv);
    end;

    // Eddystone UID.
    if cbAdvertiserEddystoneUid.Checked then begin
      Adv := TwclBluetoothLeEddystoneUidBeaconAdvertisement.Create(-5,
        BEACON_UUID);
      AddAdvertisement(Adv);
    end;

    // Eddystone URL.
    if cbAdvertiserEddystoneUrl.Checked then begin
      Adv := TwclBluetoothLeEddystoneUrlBeaconAdvertisement.Create(-5,
        'https://www.btframework.com');
      AddAdvertisement(Adv);
    end;

    if LeAdvertiser.Count = 0 then
      ShowMessage('Select at least one advertisement type.')
    else begin
      LeAdvertiser.Interval := StrToInt(edAdvertiserInterval.Text);
      Res := LeAdvertiser.Start(Radio);
      if Res <> WCL_E_SUCCESS then
        Trace('Start advertiser', Res);
    end;
  end;
end;
static const GUID BEACON_UUID = {
    0x09039835, 0x4A80, 0x443B,
    { 0x87, 0xAA, 0xDC, 0x56, 0x5D, 0x09, 0xEA, 0x61 }
};

CwclBluetoothRadio* Radio = GetRadio();
if (Radio != NULL)
{
    LeAdvertiser.Clear();

    CwclBluetoothLeAdvertisement* Adv;

    // Create and add iBeacon advertisement.
    if (cbAdvertiserBeacon.GetCheck())
    {
        Adv = new CwclBluetoothLeiBeaconAdvertisement(-5, 0x0101, 0x0202,
            BEACON_UUID);
        AddAdvertisement(Adv);
    }

    // Create and add Alt Beacon advertisement.
    if (cbAdvertiserAltBeacon.GetCheck())
    {
        Adv = new CwclBluetoothLeAltBeaconAdvertisement(-5, 0x0101, 0x0202,
            BEACON_UUID, 0xFFFE, 0x11);
        AddAdvertisement(Adv);
    }

    // Create and add Eddystone UID advertisement.
    if (cbAdvertiserEddystoneUid.GetCheck())
    {
        Adv = new CwclBluetoothLeEddystoneUidBeaconAdvertisement(-5,
            BEACON_UUID);
        AddAdvertisement(Adv);
    }

    // Create and add Eddystone URL.
    if (cbAdvertiserEddystoneUrl.GetCheck())
    {
        Adv = new CwclBluetoothLeEddystoneUrlBeaconAdvertisement(-5,
            _T("https://www.btframework.com"));
        AddAdvertisement(Adv);
    }

    if (LeAdvertiser.Count == 0)
        AfxMessageBox(_T("Select at least one advertisement type."));
    else
    {
        CString s;
        edAdvertiserInterval.GetWindowText(s);
        LeAdvertiser.Interval = LOWORD(_ttoi(s.GetBuffer()));

        int Res = LeAdvertiser.Start(Radio);
        if (Res != WCL_E_SUCCESS)
            Trace(_T("Start advertiser"), Res);
    }
}
Guid BEACON_UUID = new Guid("{09039835-4A80-443B-87AA-DC565D09EA61}");

wclBluetoothRadio Radio = GetRadio();
if (Radio != null)
{
    LeAdvertiser.Clear();

    wclBluetoothLeAdvertisement Adv;

    if (cbAdvertiserBeacon.Checked)
    {
        Adv = new wclBluetoothLeiBeaconAdvertisement(-5, 0x0101, 0x0202, BEACON_UUID);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserAltBeacon.Checked)
    {
        Adv = new wclBluetoothLeAltBeaconAdvertisement(-5, 0x0101, 0x0202,
            BEACON_UUID, 0xFFFE, 0x11);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserEddystoneUid.Checked)
    {
        Adv = new wclBluetoothLeEddystoneUidBeaconAdvertisement(-5, BEACON_UUID);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserEddystoneUrl.Checked)
    {
        Adv = new wclBluetoothLeEddystoneUrlBeaconAdvertisement(-5, "https://www.btframework.com");
        AddAdvertisement(Adv);
    }

    if (LeAdvertiser.Count == 0)
        MessageBox.Show("Select at least one advertisement type.");
    else
    {
        LeAdvertiser.Interval = Convert.ToUInt16(edAdvertiserInterval.Text);

        Int32 Res = LeAdvertiser.Start(Radio);
        if (Res != wclErrors.WCL_E_SUCCESS)
            Trace("Start advertiser", Res);
    }
}
Dim BEACON_UUID As Guid = New Guid("{09039835-4A80-443B-87AA-DC565D09EA61}")

Dim Radio As wclBluetoothRadio = GetRadio()
If Radio IsNot Nothing Then
    LeAdvertiser.Clear()

    Dim Adv As wclBluetoothLeAdvertisement

    If cbAdvertiserBeacon.Checked Then
        Adv = New wclBluetoothLeiBeaconAdvertisement(-5, &H101, &H202, BEACON_UUID)
        AddAdvertisement(Adv)
    End If

    If cbAdvertiserAltBeacon.Checked Then
        Adv = New wclBluetoothLeAltBeaconAdvertisement(-5, &H101, &H202,
            BEACON_UUID, &HFFFE, &H11)
        AddAdvertisement(Adv)
    End If

    If cbAdvertiserEddystoneUid.Checked Then
        Adv = New wclBluetoothLeEddystoneUidBeaconAdvertisement(-5, BEACON_UUID)
        AddAdvertisement(Adv)
    End If

    If cbAdvertiserEddystoneUrl.Checked Then
        Adv = New wclBluetoothLeEddystoneUrlBeaconAdvertisement(-5, "https://www.btframework.com")
        AddAdvertisement(Adv)
    End If

    If LeAdvertiser.Count = 0 Then
        MessageBox.Show("Select at least one advertisement type.")
    Else
        LeAdvertiser.Interval = Convert.ToUInt16(edAdvertiserInterval.Text)

        Dim Res As Int32 = LeAdvertiser.Start(Radio)
        If Res <> wclErrors.WCL_E_SUCCESS Then Trace("Start advertiser", Res)
    End If
End If
static const GUID BEACON_UUID = {
    0x09039835, 0x4A80, 0x443B,
    { 0x87, 0xAA, 0xDC, 0x56, 0x5D, 0x09, 0xEA, 0x61 }
};

CwclBluetoothRadio* Radio = GetRadio();
if (Radio != NULL)
{
    LeAdvertiser.Clear();

    CwclBluetoothLeAdvertisement* Adv;

    if (cbAdvertiserBeacon.GetCheck())
    {
        Adv = new CwclBluetoothLeiBeaconAdvertisement(-5, 0x0101, 0x0202, BEACON_UUID);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserAltBeacon.GetCheck())
    {
        Adv = new CwclBluetoothLeAltBeaconAdvertisement(-5, 0x0101, 0x0202,
            BEACON_UUID, 0xFFFE, 0x11);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserEddystoneUid.GetCheck())
    {
        Adv = new CwclBluetoothLeEddystoneUidBeaconAdvertisement(-5, BEACON_UUID);
        AddAdvertisement(Adv);
    }

    if (cbAdvertiserEddystoneUrl.GetCheck())
    {
        Adv = new CwclBluetoothLeEddystoneUrlBeaconAdvertisement(-5,
            _T("https://www.btframework.com"));
        AddAdvertisement(Adv);
    }

    if (LeAdvertiser.Count == 0)
        AfxMessageBox(_T("Select at least one advertisement type."));
    else
    {
        CString s;
        edAdvertiserInterval.GetWindowText(s);
        LeAdvertiser.Interval = LOWORD(_ttoi(s.GetBuffer()));

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

Manufacturer and raw frames

The manufacturer-specific and raw classes take byte arrays instead of structured identifiers. They are shown in the Custom and manufacturer-specific frames section above. The same three-step pattern applies: create, add, start.

For the wclBluetoothLeProximityBeaconAdvertisement class (which is also supported by the advertiser), the constructor additionally takes the company identifier and follows the same pattern:

if cbAdvertiserProximityBeacon.Checked then begin
  Adv := TwclBluetoothLeProximityBeaconAdvertisement.Create(-5, $0101,
    $0202, BEACON_UUID, $FFFE);
  AddAdvertisement(Adv);
end;
if (cbAdvertiserProximityBeacon.GetCheck())
{
    Adv = new CwclBluetoothLeProximityBeaconAdvertisement(-5, 0x0101, 0x0202,
        BEACON_UUID, 0xFFFE);
    AddAdvertisement(Adv);
}
if (cbAdvertiserProximityBeacon.Checked)
{
    Adv = new wclBluetoothLeProximityBeaconAdvertisement(-5, 0x0101, 0x0202,
        BEACON_UUID, 0xFFFE);
    AddAdvertisement(Adv);
}
If cbAdvertiserProximityBeacon.Checked Then
    Adv = New wclBluetoothLeProximityBeaconAdvertisement(-5, &H101, &H202, BEACON_UUID, &HFFFE)
    AddAdvertisement(Adv)
End If
if (cbAdvertiserProximityBeacon.GetCheck())
{
    Adv = new CwclBluetoothLeProximityBeaconAdvertisement(-5, 0x0101, 0x0202,
        BEACON_UUID, 0xFFFE);
    AddAdvertisement(Adv);
}

Receiving and parsing

Reception is handled by the wclBluetoothLeBeaconWatcher class. Each supported format has its own event, and each event delivers a strongly-typed payload that is already decoded — the application does not need to parse raw bytes for the standard formats.

Events

The watcher exposes a distinct event per format. Registering them takes one line each in C++ Builder and one Handles clause per method in VB.NET. The complete set that maps to the beacon formats in this article is:

  • OnAppleiBeaconFrame — decoded iBeacon.
  • OnAltBeaconFrame — decoded AltBeacon.
  • OnEddystoneUidFrame — decoded Eddystone UID.
  • OnEddystoneUrlFrame — decoded Eddystone URL, with the URL returned as a string.
  • OnEddystoneTlmFrame — decoded Eddystone TLM.
  • OnManufacturerRawFrame — any manufacturer-specific frame, delivered as company ID plus raw bytes.
  • OnMicrosoftCdpBeaconFrame — decoded Microsoft CDP.
  • OnAdvertisementRawFrame — any other AD structure, delivered as data type plus raw bytes.
  • OnAdvertisementReceived — the entire advertisement, delivered as a single raw byte stream.

Each event handler is a thin wrapper that builds a frame object and stores it. The Beacons sample is representative:

// In FormCreate - the framework wires each event through the .dfm binding,
// but the handler signatures are declared on TfmMain. The important part is
// the shape of each handler - it just constructs a frame storage object.

procedure TfmMain.BeaconWatcherAltBeaconFrame(Sender: TObject;
  const Address: Int64; const Timestamp: Int64; const Rssi: SByte;
  const CompanyId: Word; const Major: Word; const Minor: Word;
  const Uuid: TGUID; const TxRssi: SByte; const Reserved: Byte;
  const Data: TwclBluetoothLeAdvertisementFrameRawData);
var
  Frame: TFrameStorage;
begin
  Frame := TAltBeaconFrame.Create(Address, Timestamp, Rssi, Data, Uuid,
    CompanyId, Major, Minor, TxRssi, Reserved);
  AddFrame(Frame);
end;
// In OnInitDialog:
__hook(&CwclBluetoothLeBeaconWatcher::OnAppleiBeaconFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherAppleiBeaconFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnAltBeaconFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherAltBeaconFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneTlmFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneTlmFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneUidFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneUidFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneUrlFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneUrlFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnManufacturerRawFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherManufacturerRawFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnMicrosoftCdpBeaconFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherMicrosoftCdpBeaconFrame);

// Handler example:
void __fastcall TfmMain::BeaconWatcherAltBeaconFrame(TObject *Sender,
  const __int64 Address, const __int64 Timestamp, const SByte Rssi,
  const WORD CompanyId, const WORD Major, const WORD Minor,
  const TGUID &Uuid, const SByte TxRssi, const BYTE Reserved,
  const TwclBluetoothLeAdvertisementFrameRawData Data)
{
  TFrameStorage* Frame = new TAltBeaconFrame(Address, Timestamp, Rssi,
    Data, Uuid, CompanyId, Major, Minor, TxRssi, Reserved);
  AddFrame(Frame);
}
// In fmMainLoad:
BeaconWatcher.OnAppleiBeaconFrame += new wclBluetoothLeAppleiBeaconFrameEvent(BeaconWatcher_OnAppleiBeaconFrame);
BeaconWatcher.OnAltBeaconFrame += new wclBluetoothLeAltBeaconFrameEvent(BeaconWatcherAltBeaconFrame);
BeaconWatcher.OnEddystoneTlmFrame += new wclBluetoothLeEddystoneTlmFrameEvent(BeaconWatcherEddystoneTlmFrame);
BeaconWatcher.OnEddystoneUidFrame += new wclBluetoothLeEddystoneUidFrameEvent(BeaconWatcherEddystoneUidFrame);
BeaconWatcher.OnEddystoneUrlFrame += new wclBluetoothLeEddystoneUrlFrameEvent(BeaconWatcherEddystoneUrlFrame);
BeaconWatcher.OnManufacturerRawFrame += new wclBluetoothLeManufacturerRawFrameEvent(BeaconWatcherManufacturerRawFrame);
BeaconWatcher.OnMicrosoftCdpBeaconFrame += new wclBluetoothLeMicrosoftCdpBeaconFrameEvent(BeaconWatcherMicrosoftCdpBeaconFrame);

// Handler example:
void BeaconWatcherAltBeaconFrame(object Sender, long Address, long Timestamp, sbyte Rssi,
    ushort CompanyId, ushort Major, ushort Minor, Guid Uuid, sbyte TxRssi, byte Reserved,
    byte[] Data)
{
    FrameStorage Frame = new AltBeaconFrame(Address, Timestamp, Rssi, Data, Uuid,
        CompanyId, Major, Minor, TxRssi, Reserved);
    AddFrame(Frame);
}
' In fmMainLoad - VB.NET uses Handles clauses instead of explicit
' subscription. Each event handler carries a Handles clause:

Private Sub BeaconWatcherAltBeaconFrame(Sender As Object, Address As Long, Timestamp As Long,
        Rssi As SByte, CompanyId As UShort, Major As UShort, Minor As UShort, Uuid As System.Guid,
        TxRssi As SByte, Reserved As Byte, Data() As Byte) Handles BeaconWatcher.OnAltBeaconFrame
    Dim Frame As FrameStorage = New AltBeaconFrame(Address, Timestamp, Rssi, Data, Uuid,
        CompanyId, Major, Minor, TxRssi, Reserved)
    AddFrame(Frame)
End Sub
// In OnInitDialog:
__hook(&CwclBluetoothLeBeaconWatcher::OnAltBeaconFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherAltBeaconFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneTlmFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneTlmFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneUidFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneUidFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnEddystoneUrlFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherEddystoneUrlFrame);
__hook(&CwclBluetoothLeBeaconWatcher::OnManufacturerRawFrame, &BeaconWatcher,
    &CBeaconsDlg::BeaconWatcherManufacturerRawFrame);

// Handler example:
void CBeaconsDlg::BeaconWatcherAltBeaconFrame(void* Sender, const __int64 Address,
    const __int64 Timestamp, const char Rssi, const unsigned short CompanyId,
    const unsigned short Major, const unsigned short Minor, const GUID& Uuid,
    const char TxRssi, const unsigned char Reserved,
    const wclBluetoothLeAdvertisementFrameRawData& Data)
{
    UNREFERENCED_PARAMETER(Sender);

    CAltBeaconFrame* Frame = new CAltBeaconFrame(Address, Timestamp, Rssi, Data, Uuid,
        CompanyId, Major, Minor, TxRssi, Reserved);
    AddFrame(Frame);
}

Reading fields

Once the event has fired, the fields of each format can be read directly from the event parameters, or from the frame object that was stored. The Beacons sample uses the latter approach, and the code is instructive because it shows exactly what each format carries.

iBeacon

procedure TfmMain.ShowAppleiBeaconFrame(const Frame: TAppleiBeaconFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('UUID', GUIDToString(Frame.Data.Uuid));
  ShowData('Major', IntToHex(Frame.Data.Major, 4));
  ShowData('Minor', IntToHex(Frame.Data.Minor, 4));
  ShowData('TX RSSI', IntToStr(Frame.Data.TxRssi));
end;
void __fastcall TfmMain::ShowAppleiBeaconFrame(TAppleiBeaconFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("UUID", Sysutils::GUIDToString(Frame->Data.Uuid));
  ShowData("Major", IntToHex(Frame->Data.Major, 4));
  ShowData("Minor", IntToHex(Frame->Data.Minor, 4));
  ShowData("TX RSSI", IntToStr(Frame->Data.TxRssi));
}
private void ShowAppleiBeaconFrame(AppleiBeaconFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("UUID", Frame.Data.Uuid.ToString());
    ShowData("Major", Frame.Data.Major.ToString("X4"));
    ShowData("Minor", Frame.Data.Minor.ToString("X4"));
    ShowData("TX RSSI", Frame.Data.TxRssi.ToString());
}
Private Sub ShowAppleiBeaconFrame(Frame As AppleiBeaconFrame)
    ShowFrameBaseData(Frame)

    ShowData("UUID", Frame.Data.Uuid.ToString())
    ShowData("Major", Frame.Data.Major.ToString("X4"))
    ShowData("Minor", Frame.Data.Minor.ToString("X4"))
    ShowData("TX RSSI", Frame.Data.TxRssi.ToString())
End Sub
void CBeaconsDlg::ShowAppleiBeaconFrame(CAppleiBeaconFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("UUID"), GUIDToString(Frame->Data.Uuid));
    ShowData(_T("Major"), IntToHex(Frame->Data.Major, 4));
    ShowData(_T("Minor"), IntToHex(Frame->Data.Minor, 4));
    ShowData(_T("TX RSSI"), IntToStr(Frame->Data.TxRssi));
}

AltBeacon

procedure TfmMain.ShowAltBeaconFrame(const Frame: TAltBeaconFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('UUID', GUIDToString(Frame.Uuid));
  ShowData('CompanyId', IntToHex(Frame.CompanyId, 4));
  ShowData('Major', IntToHex(Frame.Major, 4));
  ShowData('Minor', IntToHex(Frame.Minor, 4));
  ShowData('TX RSSI', IntToStr(Frame.TxRssi));
  ShowData('Reserved', IntToHex(Frame.Reserved, 2));

  ShowFrameRawData(Frame.Data);
end;
void __fastcall TfmMain::ShowAltBeaconFrame(TAltBeaconFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("UUID", Sysutils::GUIDToString(Frame->Uuid));
  ShowData("CompanyId", IntToHex(Frame->CompanyId, 4));
  ShowData("Major", IntToHex(Frame->Major, 4));
  ShowData("Minor", IntToHex(Frame->Minor, 4));
  ShowData("TX RSSI", IntToStr(Frame->TxRssi));
  ShowData("Reserved", IntToHex(Frame->Reserved, 2));

  ShowFrameRawData(Frame->Data);
}
private void ShowAltBeaconFrame(AltBeaconFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("UUID", Frame.Uuid.ToString());
    ShowData("CompanyId", Frame.CompanyId.ToString("X4"));
    ShowData("Major", Frame.Major.ToString("X4"));
    ShowData("Minor", Frame.Minor.ToString("X4"));
    ShowData("TX RSSI", Frame.TxRssi.ToString());
    ShowData("Reserved", Frame.Reserved.ToString("X2"));

    ShowFrameRawData(Frame.Data);
}
Private Sub ShowAltBeaconFrame(Frame As AltBeaconFrame)
    ShowFrameBaseData(Frame)

    ShowData("UUID", Frame.Uuid.ToString())
    ShowData("CompanyId", Frame.CompanyId.ToString("X4"))
    ShowData("Major", Frame.Major.ToString("X4"))
    ShowData("Minor", Frame.Minor.ToString("X4"))
    ShowData("TX RSSI", Frame.TxRssi.ToString())
    ShowData("Reserved", Frame.Reserved.ToString("X2"))

    ShowFrameRawData(Frame.Data)
End Sub
void CBeaconsDlg::ShowAltBeaconFrame(CAltBeaconFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("UUID"), GUIDToString(Frame->Uuid));
    ShowData(_T("CompanyId"), IntToHex(Frame->CompanyId, 4));
    ShowData(_T("Major"), IntToHex(Frame->Major, 4));
    ShowData(_T("Minor"), IntToHex(Frame->Minor, 4));
    ShowData(_T("TX RSSI"), IntToStr(Frame->TxRssi));
    ShowData(_T("Reserved"), IntToHex(Frame->Reserved, 2));

    ShowFrameRawData(Frame->Data);
}

Eddystone UID

procedure TfmMain.ShowEddystoneUidFrame(const Frame: TEddystoneUidFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('UUID', GUIDToString(Frame.Uuid));
  ShowData('TX RSSI', IntToStr(Frame.TxRssi));

  ShowFrameRawData(Frame.Data);
end;
void __fastcall TfmMain::ShowEddystoneUidFrame(TEddystoneUidFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("UUID", Sysutils::GUIDToString(Frame->Uuid));
  ShowData("TX RSSI", IntToStr(Frame->TxRssi));

  ShowFrameRawData(Frame->Data);
}
private void ShowEddystoneUidFrame(EddystoneUidFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("UUID", Frame.Uuid.ToString());
    ShowData("TX RSSI", Frame.TxRssi.ToString());

    ShowFrameRawData(Frame.Data);
}
Private Sub ShowEddystoneUidFrame(Frame As EddystoneUidFrame)
    ShowFrameBaseData(Frame)

    ShowData("UUID", Frame.Uuid.ToString())
    ShowData("TX RSSI", Frame.TxRssi.ToString())

    ShowFrameRawData(Frame.Data)
End Sub
void CBeaconsDlg::ShowEddystoneUidFrame(CEddystoneUidFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("UUID"), GUIDToString(Frame->Uuid));
    ShowData(_T("TX RSSI"), IntToStr(Frame->TxRssi));

    ShowFrameRawData(Frame->Data);
}

Eddystone URL

procedure TfmMain.ShowEddystoneUrlFrame(const Frame: TEddystoneUrlFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('TX RSSI', IntToStr(Frame.TxRssi));
  ShowData('URL', Frame.Url);
end;
void __fastcall TfmMain::ShowEddystoneUrlFrame(TEddystoneUrlFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("TX RSSI", IntToStr(Frame->TxRssi));
  ShowData("URL", Frame->Url);
}
private void ShowEddystoneUrlFrame(EddystoneUrlFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("TX RSSI", Frame.TxRssi.ToString());
    ShowData("URL", Frame.Url);
}
Private Sub ShowEddystoneUrlFrame(Frame As EddystoneUrlFrame)
    ShowFrameBaseData(Frame)

    ShowData("TX RSSI", Frame.TxRssi.ToString())
    ShowData("URL", Frame.Url)
End Sub
void CBeaconsDlg::ShowEddystoneUrlFrame(CEddystoneUrlFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("TX RSSI"), IntToStr(Frame->TxRssi));
    ShowData(_T("URL"), CString(Frame->Url.c_str()));
}

Eddystone TLM

procedure TfmMain.ShowEddystoneTlmFrame(const Frame: TEddystoneTlmFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('Advertisements Counter', IntToStr(Frame.AdvCnt));
  ShowData('Battery status', IntToStr(Frame.Batt));
  ShowData('Seconds', IntToStr(Frame.SecCnt));
  ShowData('Temperature', FloatToStr(Frame.Temp));

  ShowFrameRawData(Frame.Data);
end;
void __fastcall TfmMain::ShowEddystoneTlmFrame(TEddystoneTlmFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("Advertisements Counter", IntToStr((int)Frame->AdvCnt));
  ShowData("Battery status", IntToStr(Frame->Batt));
  ShowData("Seconds", IntToStr((int)Frame->SecCnt));
  ShowData("Temperature", FloatToStr(Frame->Temp));

  ShowFrameRawData(Frame->Data);
}
private void ShowEddystoneTlmFrame(EddystoneTlmFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("Advertisements Counter", Frame.AdvCnt.ToString());
    ShowData("Battery status", Frame.Batt.ToString());
    ShowData("Seconds", Frame.SecCnt.ToString());
    ShowData("Temperature", Frame.Temp.ToString());

    ShowFrameRawData(Frame.Data);
}
Private Sub ShowEddystoneTlmFrame(Frame As EddystoneTlmFrame)
    ShowFrameBaseData(Frame)

    ShowData("Advertisements Counter", Frame.AdvCnt.ToString())
    ShowData("Battery status", Frame.Batt.ToString())
    ShowData("Seconds", Frame.SecCnt.ToString())
    ShowData("Temperature", Frame.Temp.ToString())

    ShowFrameRawData(Frame.Data)
End Sub
void CBeaconsDlg::ShowEddystoneTlmFrame(CEddystoneTlmFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("Advertisements Counter"), IntToStr(Frame->AdvCnt));
    ShowData(_T("Battery status"), IntToStr(Frame->Batt));
    ShowData(_T("Seconds"), IntToStr(Frame->SecCnt));
    ShowData(_T("Temperature"), FloatToStr(Frame->Temp));

    ShowFrameRawData(Frame->Data);
}

Manufacturer-specific data

Manufacturer-specific frames arrive through OnManufacturerRawFrame. The company identifier is already decoded and the payload is available as a raw byte array — the application decides how to interpret it.

procedure TfmMain.ShowManufacturerFrame(const Frame: TManufacturerFrame);
begin
  ShowFrameBaseData(Frame);

  ShowData('Company ID', IntToHex(Frame.CompanyId, 4));

  ShowFrameRawData(Frame.Data);
end;
void __fastcall TfmMain::ShowManufacturerFrame(TManufacturerFrame* const Frame)
{
  ShowFrameBaseData(Frame);

  ShowData("Company ID", IntToHex(Frame->CompanyId, 4));

  ShowFrameRawData(Frame->Data);
}
private void ShowManufacturerFrame(ManufacturerFrame Frame)
{
    ShowFrameBaseData(Frame);

    ShowData("Company ID", Frame.CompanyId.ToString("X4"));

    ShowFrameRawData(Frame.Data);
}
Private Sub ShowManufacturerFrame(Frame As ManufacturerFrame)
    ShowFrameBaseData(Frame)

    ShowData("Company ID", Frame.CompanyId.ToString("X4"))

    ShowFrameRawData(Frame.Data)
End Sub
void CBeaconsDlg::ShowManufacturerFrame(CManufacturerFrame* const Frame)
{
    ShowFrameBaseData(Frame);

    ShowData(_T("Company ID"), IntToHex(Frame->CompanyId, 4));

    ShowFrameRawData(Frame->Data);
}

Two more events exist for frames that do not fit any of the standard classes. OnAdvertisementRawFrame delivers individual AD structures that the framework did not decode — it carries the data type byte and the raw payload. OnAdvertisementReceived delivers the complete advertisement as a single byte stream, including all AD structures, and is the escape hatch for applications that need the raw bytes end to end.


Combining formats

The most useful property of the advertiser is that it does not force a single format. A single radio can broadcast several advertisement objects in the same interval. This is exactly what the Beacons sample does — every checkbox that is ticked adds one more advertisement to the same advertiser.

Three practical combinations are common in the field:

  • iBeacon + Eddystone URL — one payload for native iOS applications that already understand iBeacon, and a second payload for browsers and passive scanners that can present a link. Both audiences are served by one device.
  • Eddystone UID + Eddystone TLM — the UID identifies the beacon, and the TLM frames report battery and uptime. Fleet managers can monitor the deployment without visiting each physical device.
  • Manufacturer-specific + iBeacon — a proprietary protocol for the vendor's own tooling, broadcast alongside iBeacon so that generic beacon apps still see the device.

There is one caveat. Legacy advertising has a 31-byte limit for the whole packet, so if two formats are combined and the combined payload exceeds 31 bytes, the framework may not be able to include both. On Bluetooth 5 hardware the application can switch to extended advertising, which removes the size limit but requires the receiver to also support the extended format.


Troubleshooting

Advertisement does not fit in 31 bytes. Combining iBeacon with Eddystone URL or with a manufacturer-specific frame can easily exceed the legacy limit. On Bluetooth 5 hardware, enable extended advertising. On older hardware, reduce the number of formats or shorten the payload.

Manufacturer-specific frame is received but the company ID is not recognised. The receiver's parser does not know how to interpret that specific company identifier's convention. This is expected — the sender must either use a company identifier that the receiver already understands, or the receiver must be updated to add a parser for the new company.

Eddystone URL is delivered as raw bytes instead of a decoded string. Some framings — for example an Eddystone URL with a custom scheme byte — cannot be decoded as a URL, and the framework falls back to delivering the raw payload. Check that the URL uses one of the four standard scheme prefixes.

iBeacon TX power field seems wrong. The value is not measured. It is the declared transmit power at the antenna at one metre, as configured by the beacon manufacturer. Different hardware has different calibration accuracy. Receivers that use the field for distance estimation should treat it as approximate.

The raw frame is rejected by the driver. Raw frames must be well-formed AD structures — the length byte must match the actual payload length, and the AD type byte must be a value the driver allows. Driver restrictions are documented in Bluetooth LE Advertisement.


Frequently asked questions

Which BLE beacon format should I use?
Use iBeacon for the widest compatibility, Eddystone URL for direct web-link broadcasting, Eddystone TLM when you need remote telemetry, AltBeacon for an open specification, and manufacturer-specific frames when the payload is proprietary.
Can a device transmit several beacon formats at once?
Yes. The Bluetooth Framework's wclBluetoothLeAdvertiser class accepts multiple advertisement objects, so an application can broadcast iBeacon, Eddystone, and manufacturer-specific frames from the same radio in the same advertising interval.
What is the difference between manufacturer-specific data and a custom raw frame?
Manufacturer-specific data uses the standard AD type 0xFF with a registered company identifier, so the receiver already knows how to route the payload. A custom raw frame is a fully application-defined AD structure with no assumed meaning — the receiver must be told how to decode it.
Do I need a registered company identifier?
Yes, for any real deployment. The Bluetooth SIG assigns 16-bit company identifiers to member organisations. Using an unassigned value is technically possible on the wire but will confuse receivers that expect the format to follow a known convention.