A BLE beacon is a device that periodically transmits data without accepting a connection.
A room thermometer can simply broadcast “24 ºC” without accepting commands. A museum beacon can publish an identifier that allows proximity estimation.
For this purpose, Beacons (or Broadcasters) exist. These are devices that do not accept connections. They simply broadcast their data into the air periodically and go back to sleep.
This is the most efficient way to transmit small amounts of data (basic telemetry) with the lowest possible power consumption.
Broadcaster vs Peripheral
In the GAP terminology we covered:
- Peripheral: Advertises and waits for someone to connect.
- Broadcaster: Advertises and doesn’t care if anyone is listening or not.
In Zephyr, the main code difference lies in the advertising type (BT_LE_ADV_NCONN instead of BT_LE_ADV_CONN) and how we structure the data.
The Payload: Manufacturer Data
The legacy advertising packet we will use has a maximum of 31 bytes. This is small, but sufficient to send an ID, temperature, and battery level; extended advertisements support larger payloads.
To send custom data, the Bluetooth standard defines a data type called Manufacturer Specific Data.
This field has a structure:
- Company ID (2 bytes): A code assigned by the Bluetooth SIG to each company (Apple is 0x004C, Nordic is 0x0059).
- Data (N bytes): Whatever we want to send.
For testing and development, the standard reserves the ID 0xFFFF. We’ll use this to avoid violating protocol rules while learning.
Code: A Temperature Beacon
Let’s create a device that simulates reading a temperature and updates it in the advertising packet every second.
Define the Data Structure
First, we define what we want to send. We’ll send a counter and a simulated floating-point temperature value.
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/sys/byteorder.h>
/* Define our test Company ID */
#define TEST_COMPANY_ID 0xFFFF
/* Buffer with explicit format: Company ID, Device ID, and temperature x100 */
static uint8_t mfg_data[6];
static int16_t temperature = 2500;
static void encode_mfg_data(void)
{
sys_put_le16(TEST_COMPANY_ID, &mfg_data[0]);
sys_put_le16(0x1234, &mfg_data[2]);
sys_put_le16((uint16_t)temperature, &mfg_data[4]);
}Define the Advertising Packet
We create the bt_data array. Notice we use the BT_DATA_MANUFACTURER_DATA type.
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR), /* Non-connectable */
/* Here we put our "raw" structure */
BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)),
};Start the Broadcaster
In main, we initialize the stack and start advertising.
Important: We use BT_LE_ADV_NCONN (Non-Connectable). If you try to connect to this device with your phone, it will error out or the “Connect” button may not even appear.
int main(void)
{
int err;
err = bt_enable(NULL);
if (err) {
printk("Error bt_enable (err %d)\n", err);
return err;
}
encode_mfg_data();
/* Start NON-CONNECTABLE advertising */
/* Parameters:
- Options: BT_LE_ADV_OPT_NONE
- Min Interval: 800 (x 0.625ms = 500ms)
- Max Interval: 801
*/
struct bt_le_adv_param *adv_param =
BT_LE_ADV_PARAM(BT_LE_ADV_OPT_NONE, 800, 801, NULL);
err = bt_le_adv_start(adv_param, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Advertising error (err %d)\n", err);
return err;
}
printk("Beacon started. Sending data...\n");
/* Main loop: Dynamically update data */
while (1) {
k_sleep(K_SECONDS(2));
/* Simulate temperature change */
temperature += 10; // +0.1 ºC
if (temperature > 3000) {
temperature = 2000;
}
encode_mfg_data();
printk("Updating temperature to: %d\n", temperature);
/* Hot update the advertisement */
err = bt_le_adv_update_data(ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Update failed (err %d)\n", err);
}
}
}Analyzing the Result with nRF Connect
Compile and flash this code; then open nRF Connect on your phone.
- Go to the Scanner tab.
- Look for a device (it will likely appear as “N/A” or with a name if you added one, though pure beacons sometimes omit the name to save bytes).
- Expand the details.
- Look for the Manufacturer Data field.
You will see something like: 0xFFFF3412C409.
FFFF: Company ID.3412: Device ID0x1234encoded in little-endian.C409: 2500 encoded in little-endian (25.00 ºC).
You’ll see the value change every 2 seconds without needing to connect!
Eddystone and iBeacon
You might be familiar with iBeacon (Apple) or Eddystone (Google). These are formats defined on top of the same advertising mechanism we just used.
Instead of inventing a my_beacon_data_t structure, Apple and Google defined: “Byte 1 is the UUID, byte 2 is the power, byte 3 is the URL…”.
In Zephyr, implementing an iBeacon is simply filling the bt_data array with the bytes dictated by Apple’s specification.
Power Consumption
A Broadcaster is the king of battery life.
- The radio only turns on for a few milliseconds every 500ms (or whatever interval we define).
- It doesn’t need to listen (RX) waiting for connection requests.
With long intervals and low-power design, a coin cell battery can last a very long time. Actual autonomy depends on the SoC, radio power, interval, sensors, and usable battery capacity.