BLE Basics
Zephyr has a full BLE stack (based on Bluetooth Mesh / HCI). On ESP32, it's first-class: Espressif uses Zephyr as the basis for nRF Connect SDK.
This page covers the minimum viable BLE peripheral: advertise, accept a connection, and serve sensor data via a custom GATT characteristic.
Kconfig
prj.conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="EmbeddedFun-01"
CONFIG_BT_DEVICE_APPEARANCE=768 # "Generic Sensor"
CONFIG_BT_MAX_CONN=1
Advertising
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gap.h>
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
int main(void)
{
bt_enable(NULL);
bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), NULL, 0);
/* ... */
}
Custom GATT service
Define a service with a single readable+notifiable characteristic:
#include <zephyr/bluetooth/gatt.h>
/* Custom 128-bit UUIDs */
#define BT_UUID_CUSTOM_SVC BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0)
#define BT_UUID_TEMP_CHAR BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1)
static float g_temperature = 0.0f;
static ssize_t read_temp(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
return bt_gatt_attr_read(conn, attr, buf, len, offset,
&g_temperature, sizeof(g_temperature));
}
BT_GATT_SERVICE_DEFINE(sensor_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(BT_UUID_CUSTOM_SVC)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(BT_UUID_TEMP_CHAR),
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_temp, NULL, &g_temperature),
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
/* Update value and notify connected clients */
void ble_update_temperature(float temp)
{
g_temperature = temp;
bt_gatt_notify(NULL, &sensor_svc.attrs[1], &g_temperature, sizeof(g_temperature));
}
Testing with nRF Connect
- Flash your firmware
- Open nRF Connect app (iOS or Android)
- Scan — find
EmbeddedFun-01 - Connect → expand the custom service → read the characteristic
- Enable notifications → see values update in real time
Connection callbacks
static void connected(struct bt_conn *conn, uint8_t err)
{
if (err) {
printk("Connection failed (err %u)\n", err);
} else {
printk("Connected\n");
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
printk("Disconnected (reason %u)\n", reason);
/* Restart advertising */
bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), NULL, 0);
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
tip
Use CONFIG_BT_DEVICE_NAME in prj.conf to set the advertised name. It must be ≤ 29 bytes to fit in the advertising payload without an extended advertisement.