zephyr-bluetooth-gatt-servicios

Bluetooth GATT in Zephyr: Services and Characteristics

  • 5 min

A GATT service is a set of attributes that organizes the data and operations of a Bluetooth LE device.

When you connect your smartwatch to your phone, you don’t just see the time. The watch sends your heart rate in real-time, and you can send it configurations or change the watch face from the App. That’s a bidirectional connection based on GATT (Generic Attribute Profile).

In Zephyr, defining this structure is incredibly elegant thanks to its macro system. Let’s build a real example: a Sensor Service that sends temperature data to the phone and allows controlling an LED from the App.

The structure: services and characteristics

We can think of GATT as a database organized into several levels:

Profile: It’s the complete collection (e.g., “Heart Rate Monitor”).

Service: It’s a logical folder (e.g., “Heart Rate Measurement”, “Device Information”).

Characteristic: It’s the file with the data (e.g., “BPM Value”, “Sensor Location”).

Each characteristic has:

  • Value: The data itself.
  • Properties: Can it be read (READ)? Can it be written (WRITE)? Does it notify when it changes (NOTIFY)?
  • Descriptors: Metadata (e.g., the unit of measurement, or the famous CCC to enable notifications).

UUIDs (Identifiers)

Since we are creating our own service (it’s not a standard like the Battery service), we need to generate random 128-bit UUIDs to avoid collisions with anyone else.

You can use an online generator (search for “UUID generator”).

#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>

/* Service UUID: 12345678-1234-5678-1234-56789abcdef0 */
#define MY_SERVICE_UUID_VAL \
	BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0)

/* Temperature Characteristic UUID */
#define MY_CHAR_UUID_VAL \
	BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1)

static struct bt_uuid_128 my_service_uuid = BT_UUID_INIT_128(MY_SERVICE_UUID_VAL);
static struct bt_uuid_128 my_char_uuid = BT_UUID_INIT_128(MY_CHAR_UUID_VAL);
Copied!

Define the service using macros

Zephyr allows declaring this structure statically using the BT_GATT_SERVICE_DEFINE macro.

Let’s define a characteristic that is:

  1. Readable: The phone can query the value.
  2. Notifiable: The device pushes the value to the phone when it changes.
/* Variable where we store the actual data */
static uint32_t temperature_value = 0;

/* Read callback: Called when the phone requests to read the data */
static ssize_t read_temperature(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			 void *buf, uint16_t len, uint16_t offset)
{
    /* Send the current value of the variable */
    const char *value = (const char *)&temperature_value;
    return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(temperature_value));
}

/* Callback when the notification configuration (CCC) changes */
static void my_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    bool notif_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Notifications %s\n", notif_enabled ? "enabled" : "disabled");
}

/* SERVICE DEFINITION */
BT_GATT_SERVICE_DEFINE(my_sensor_svc,
    /* 1. Declare it as a Primary Service */
    BT_GATT_PRIMARY_SERVICE(&my_service_uuid.uuid),

    /* 2. Define the Characteristic */
    /* Parameters: UUID, Properties, Permissions, Read Callback, Write Callback, Initial Value */
    BT_GATT_CHARACTERISTIC(&my_char_uuid.uuid,
			   BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, /* Properties */
			   BT_GATT_PERM_READ,                       /* Permissions (Security) */
			   read_temperature, NULL, NULL),

    /* 3. Define the CCC Descriptor (Client Characteristic Configuration) */
    /* MANDATORY if we use NOTIFY or INDICATE */
    BT_GATT_CCC(my_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
Copied!

The CCC is essential for notifications. If you set BT_GATT_CHRC_NOTIFY but forget the BT_GATT_CCC macro, the client won’t be able to subscribe to changes.

The main code (main)

Now we need to:

  1. Initialize Bluetooth.
  2. Start advertising (as Connectable).
  3. Simulate temperature changes and Notify.
/* Advertising data (now we include the service UUID so the phone knows what we do) */
static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, MY_SERVICE_UUID_VAL),
};

int main(void)
{
    int err;

    err = bt_enable(NULL);
    if (err) {
        printk("Error init (err %d)\n", err);
        return err;
    }

    /* Advertise as CONNECTABLE (BT_LE_ADV_CONN) */
    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
    if (err) {
        printk("Error advertising (err %d)\n", err);
        return err;
    }

    printk("Ready. Connect with nRF Connect.\n");

    while (1) {
        k_sleep(K_SECONDS(1));

        /* Simulate sensor reading */
        temperature_value++; 

        /* NOTIFY (PUSH) */
        /* Find the attribute within the service and send the change */
        /* attrs[1] is the declaration and attrs[2] contains the value. */
        /* Use bt_gatt_notify(conn, attr, data, len) */
        /* If conn is NULL, it notifies ALL connected devices */
        bt_gatt_notify(NULL, &my_sensor_svc.attrs[2], &temperature_value,
                       sizeof(temperature_value));
    }
}
Copied!

Test with your phone

  1. Open nRF Connect.
  2. Scan and look for your device (you’ll see it advertising a long UUID).
  3. Tap CONNECT.
  4. A new tab will open. You’ll see “Generic Access”, “Generic Attribute” and your Unknown Service (with your UUID).
  5. Tap on the service. You’ll see the characteristic.
  6. Tap the Download icon (down arrow): you will read the current value.
  7. Tap the subscribe icon: the value will start updating every second without manual reads.

Receiving data (write)

What if we want to turn on an LED from the phone? We need a characteristic with the WRITE property.

/* Write callback */
static ssize_t write_led(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			 const void *buf, uint16_t len, uint16_t offset,
			 uint8_t flags)
{
    const uint8_t *val = buf;

    if (offset != 0) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    if (len != 1) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    if (*val == 1) {
        printk("LED ON from phone\n");
        // gpio_pin_set(...)
    } else {
        printk("LED OFF from phone\n");
    }

    return len;
}

/* In the service definition we add: */
BT_GATT_CHARACTERISTIC(&my_led_uuid.uuid,
           BT_GATT_CHRC_WRITE,
           BT_GATT_PERM_WRITE,
           NULL, write_led, NULL),
Copied!

In nRF Connect, you will see an “Up arrow” icon. By tapping it, you can send a byte (01 or 00) and you will see the message in the Zephyr console.