esp-idf/examples/bluetooth/ble_get_started/nimble/NimBLE_GATT_Server
2024-08-30 11:45:36 +08:00
..
main fix(ble): Increased the length of addr_str in README.md 2024-08-30 11:45:36 +08:00
CMakeLists.txt docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
README.md docs(ble): Improved as Weilong and Shenhang requested 2024-08-22 09:55:22 +08:00
sdkconfig.defaults docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32c3 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32c5 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32c6 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32h2 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32s2 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00
sdkconfig.defaults.esp32s3 docs(ble): Added BLE Get Started 2024-08-22 09:55:22 +08:00

Supported Targets ESP32 ESP32-C2 ESP32-C3 ESP32-C5 ESP32-C6 ESP32-H2 ESP32-S3

NimBLE GATT Server Example

Overview

This example is extended from NimBLE Connection Example, and further introduces

  1. How to implement a GATT server using GATT services table
  2. How to handle characteristic access requests
    1. Write access demonstrated by LED control
    2. Read and indicate access demonstrated by heart rate measurement(mocked)

It uses ESP32's Bluetooth controller and NimBLE host stack.

To test this demo, any BLE scanner application can be used.

Try It Yourself

Set Target

Before project configuration and build, be sure to set the correct chip target using:

idf.py set-target <chip_name>

For example, if you're using ESP32, then input

idf.py set-target esp32

Build and Flash

Run the following command to build, flash and monitor the project.

idf.py -p <PORT> flash monitor

For example, if the corresponding serial port is /dev/ttyACM0, then it goes

idf.py -p /dev/ttyACM0 flash monitor

(To exit the serial monitor, type Ctrl-].)

See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.

Code Explained

Overview

  1. Initialization
    1. Initialize LED, NVS flash, NimBLE host stack, GAP service
    2. Initialize GATT service and add services to registration queue
    3. Configure NimBLE host stack and start NimBLE host task thread, GATT services will be registered automatically when NimBLE host stack started
    4. Start heart rate update task thread
  2. Wait for NimBLE host stack to sync with BLE controller, and start advertising; wait for connection event to come
  3. After connection established, wait for GATT characteristics access events to come
    1. On write LED event, turn on or off the LED accordingly
    2. On read heart rate event, send out current heart rate measurement value
    3. On indicate heart rate event, enable heart rate indication

Entry Point

In this example, we call GATT gatt_svr_init function to initialize GATT server in app_main before NimBLE host configuration. This is a custom function defined in gatt_svc.c, and basically we just call GATT service initialization API and add services to registration queue.

And there's another code added in nimble_host_config_init, which is

static void nimble_host_config_init(void) {
    ...

    ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;

    ...
}

That is GATT server register callback function. In this case it will only print out some registration information when services, characteristics or descriptors are registered.

Then, after NimBLE host task thread is created, we'll create another task defined in heart_rate_task to update heart rate measurement mock value and send indication if enabled.

GAP Service Updates

gap_event_handler is updated with LED control removed, and more event handling branches, when compared to NimBLE Connection Example, including

  • BLE_GAP_EVENT_ADV_COMPLETE - Advertising complete event
  • BLE_GAP_EVENT_NOTIFY_TX - Notificate event
  • BLE_GAP_EVENT_SUBSCRIBE - Subscribe event
  • BLE_GAP_EVENT_MTU - MTU update event

BLE_GAP_EVENT_ADV_COMPLETE and BLE_GAP_EVENT_MTU events are actually not involved in this example, but we still put them down there for reference. BLE_GAP_EVENT_NOTIFY_TX and BLE_GAP_EVENT_SUBSCRIBE events will be discussed in the next section.

GATT Services Table

GATT services are defined in ble_gatt_svc_def struct array, with a variable name gatt_svr_svcs in this demo. We'll call it as the GATT services table in the following content.

/* Heart rate service */
static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D);

static uint8_t heart_rate_chr_val[2] = {0};
static uint16_t heart_rate_chr_val_handle;
static const ble_uuid16_t heart_rate_chr_uuid = BLE_UUID16_INIT(0x2A37);

static uint16_t heart_rate_chr_conn_handle = 0;
static bool heart_rate_chr_conn_handle_inited = false;
static bool heart_rate_ind_status = false;

/* Automation IO service */
static const ble_uuid16_t auto_io_svc_uuid = BLE_UUID16_INIT(0x1815);
static uint16_t led_chr_val_handle;
static const ble_uuid128_t led_chr_uuid =
    BLE_UUID128_INIT(0x23, 0xd1, 0xbc, 0xea, 0x5f, 0x78, 0x23, 0x15, 0xde, 0xef,
                     0x12, 0x12, 0x25, 0x15, 0x00, 0x00);

/* GATT services table */
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
    /* Heart rate service */
    {.type = BLE_GATT_SVC_TYPE_PRIMARY,
     .uuid = &heart_rate_svc_uuid.u,
     .characteristics =
         (struct ble_gatt_chr_def[]){
             {/* Heart rate characteristic */
              .uuid = &heart_rate_chr_uuid.u,
              .access_cb = heart_rate_chr_access,
              .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_INDICATE,
              .val_handle = &heart_rate_chr_val_handle},
             {
                 0, /* No more characteristics in this service. */
             }}},

    /* Automation IO service */
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = &auto_io_svc_uuid.u,
        .characteristics =
            (struct ble_gatt_chr_def[]){/* LED characteristic */
                                        {.uuid = &led_chr_uuid.u,
                                         .access_cb = led_chr_access,
                                         .flags = BLE_GATT_CHR_F_WRITE,
                                         .val_handle = &led_chr_val_handle},
                                        {0}},
    },
    
    {
        0, /* No more services. */
    },
};

In this table, there are two GATT primary services defined

  • Heart rate service with a UUID of 0x180D
  • Automation IO service with a UUID of 0x1815

Automation IO Service

Under automation IO service, there's a LED characteristic, with a vendor-specific UUID and write only permission.

The characteristic is binded with led_chr_access callback function, in which the write access event is captured. The LED will be turned on or off according to the write value, quite straight-forward.

static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
                          struct ble_gatt_access_ctxt *ctxt, void *arg) {
    /* Local variables */
    int rc;

    /* Handle access events */
    /* Note: LED characteristic is write only */
    switch (ctxt->op) {

    /* Write characteristic event */
    case BLE_GATT_ACCESS_OP_WRITE_CHR:
        /* Verify connection handle */
        if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
            ESP_LOGI(TAG, "characteristic write; conn_handle=%d attr_handle=%d",
                     conn_handle, attr_handle);
        } else {
            ESP_LOGI(TAG,
                     "characteristic write by nimble stack; attr_handle=%d",
                     attr_handle);
        }

        /* Verify attribute handle */
        if (attr_handle == led_chr_val_handle) {
            /* Verify access buffer length */
            if (ctxt->om->om_len == 1) {
                /* Turn the LED on or off according to the operation bit */
                if (ctxt->om->om_data[0]) {
                    led_on();
                    ESP_LOGI(TAG, "led turned on!");
                } else {
                    led_off();
                    ESP_LOGI(TAG, "led turned off!");
                }
            } else {
                goto error;
            }
            return rc;
        }
        goto error;

    /* Unknown event */
    default:
        goto error;
    }

error:
    ESP_LOGE(TAG,
             "unexpected access operation to led characteristic, opcode: %d",
             ctxt->op);
    return BLE_ATT_ERR_UNLIKELY;
}

Heart Rate Service

Under heart rate service, there's a heart rate measurement characteristic, with a UUID of 0x2A37 and read + indicate access permission.

The characteristic is binded with heart_rate_chr_access callback function, in which the read access event is captured. It should be mentioned that in SIG definition, heart rate measurement is a multi-byte data structure, with the first byte indicating the flags

  • Bit 0: Heart rate value type
    • 0: Heart rate value is uint8_t type
    • 1: Heart rate value is uint16_t type
  • Bit 1: Sensor contact status
  • Bit 2: Sensor contact supported
  • Bit 3: Energy expended status
  • Bit 4: RR-interval status
  • Bit 5-7: Reserved

and the rest of bytes are data fields. In this case, we use uint8_t type and disable other features, making the characteristic value a 2-byte array. So when characteristic read event arrives, we'll get the latest heart rate value and send it back to peer device.

static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
                                 struct ble_gatt_access_ctxt *ctxt, void *arg) {
    /* Local variables */
    int rc;

    /* Handle access events */
    /* Note: Heart rate characteristic is read only */
    switch (ctxt->op) {

    /* Read characteristic event */
    case BLE_GATT_ACCESS_OP_READ_CHR:
        /* Verify connection handle */
        if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
            ESP_LOGI(TAG, "characteristic read; conn_handle=%d attr_handle=%d",
                     conn_handle, attr_handle);
        } else {
            ESP_LOGI(TAG, "characteristic read by nimble stack; attr_handle=%d",
                     attr_handle);
        }

        /* Verify attribute handle */
        if (attr_handle == heart_rate_chr_val_handle) {
            /* Update access buffer value */
            heart_rate_chr_val[1] = get_heart_rate();
            rc = os_mbuf_append(ctxt->om, &heart_rate_chr_val,
                                sizeof(heart_rate_chr_val));
            return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
        }
        goto error;

    /* Unknown event */
    default:
        goto error;
    }

error:
    ESP_LOGE(
        TAG,
        "unexpected access operation to heart rate characteristic, opcode: %d",
        ctxt->op);
    return BLE_ATT_ERR_UNLIKELY;
}

Indicate access, however, is a bit more complicated. As mentioned in GAP Service Updates, we'll handle another 2 events namely BLE_GAP_EVENT_NOTIFY_TX and BLE_GAP_EVENT_SUBSCRIBE in gap_event_handler. In this case, if peer device wants to enable heart rate measurement indication, it will send a subscribe request to the local device, and the request will be captured as a subscribe event in gap_event_handler. But from the perspective of software layering, the event should be handled in GATT server, so we just pass the event to GATT server by calling gatt_svr_subscribe_cb, as demonstrated in the demo

static int gap_event_handler(struct ble_gap_event *event, void *arg) {
    ...

    /* Subscribe event */
    case BLE_GAP_EVENT_SUBSCRIBE:
        /* Print subscription info to log */
        ESP_LOGI(TAG,
                    "subscribe event; conn_handle=%d attr_handle=%d "
                    "reason=%d prevn=%d curn=%d previ=%d curi=%d",
                    event->subscribe.conn_handle, event->subscribe.attr_handle,
                    event->subscribe.reason, event->subscribe.prev_notify,
                    event->subscribe.cur_notify, event->subscribe.prev_indicate,
                    event->subscribe.cur_indicate);

        /* GATT subscribe event callback */
        gatt_svr_subscribe_cb(event);
        return rc;
    
    ...
}

Then we'll check connection handle and attribute handle, if the attribute handle matches heart_rate_chr_val_chandle, heart_rate_chr_conn_handle and heart_rate_ind_status will be updated together.

void gatt_svr_subscribe_cb(struct ble_gap_event *event) {
    /* Check connection handle */
    if (event->subscribe.conn_handle != BLE_HS_CONN_HANDLE_NONE) {
        ESP_LOGI(TAG, "subscribe event; conn_handle=%d attr_handle=%d",
                 event->subscribe.conn_handle, event->subscribe.attr_handle);
    } else {
        ESP_LOGI(TAG, "subscribe by nimble stack; attr_handle=%d",
                 event->subscribe.attr_handle);
    }

    /* Check attribute handle */
    if (event->subscribe.attr_handle == heart_rate_chr_val_handle) {
        /* Update heart rate subscription status */
        heart_rate_chr_conn_handle = event->subscribe.conn_handle;
        heart_rate_chr_conn_handle_inited = true;
        heart_rate_ind_status = event->subscribe.cur_indicate;
    }
}

Notice that heart rate measurement incation is handled in heart_rate_task by calling send_heart_rate_indication function periodically. Actually, this function will check heart rate indication status and send indication accordingly. In this way, heart rate indication is implemented.

void send_heart_rate_indication(void) {
    if (heart_rate_ind_status && heart_rate_chr_conn_handle_inited) {
        ble_gatts_indicate(heart_rate_chr_conn_handle,
                           heart_rate_chr_val_handle);
        ESP_LOGI(TAG, "heart rate indication sent!");
    }
}

static void heart_rate_task(void *param) {
    /* Task entry log */
    ESP_LOGI(TAG, "heart rate task has been started!");

    /* Loop forever */
    while (1) {
        /* Update heart rate value every 1 second */
        update_heart_rate();
        ESP_LOGI(TAG, "heart rate updated to %d", get_heart_rate());

        /* Send heart rate indication if enabled */
        send_heart_rate_indication();

        /* Sleep */
        vTaskDelay(HEART_RATE_TASK_PERIOD);
    }

    /* Clean up at exit */
    vTaskDelete(NULL);
}

Observation

If everything goes well, you should be able to see 4 services when connected to ESP32, including

  • Generic Access
  • Generic Attribute
  • Heart Rate
  • Automation IO

Click on Automation IO Service, you should be able to see LED characteristic. Click on upload button, you should be able to write ON or OFF value. Send it to the device, LED will be turned on or off following your instruction.

Click on Heart Rate Service, you should be able to see Heart Rate Measurement characteristic. Click on download button, you should be able to see the latest heart rate measurement mock value, and it should be consistent with what is shown on serial output. Click on subscribe button, you should be able to see the heart rate measurement mock value updated every second.

Troubleshooting

For any technical queries, please file an issue on GitHub. We will get back to you soon.