Bluetooth Core

Overview

The Bluetooth Core module provides a set of functions to setup the Bluetooth Low Energy (BLE) stack and manage the BLE connection.

it uses the NimBLE stack, which is a fully featured Bluetooth 4.2 stack that is included in the ESP-IDF.

it is based of the NimBLE Peripheral Example

it has been modified to support multiple connections, and to be split into 3 module files.

it consists of 3 main components:

  • bluetooth.c: the main module that initializes the NimBLE stack, creates the bluetooth user task, and sets up the NimBLE host, and device name.
  • gap.c: the Generic Access Profile (GAP) module that sets up the BLE advertising data, and manages the connections.
  • gatt.c: the Generic Attribute Profile (GATT) module that sets up the BLE services, and characteristics, and passes GATT events to their respective handlers.

these 3 sections will be explained 1 by 1 in the following sections.

bluetooth.c

Variables

own_addr_type

the type of address used by the device. currently allways set to BLE_OWN_ADDR_PUBLIC, but is used as a pointer to variable in many Nimble functions, so it is kept as a variable.

uint8_t own_addr_type;

Public Functions

bluetooth_init

initializes the bluetooth task, and the NimBLE stack.

void bluetooth_init(void)
{
    int rc;

    ESP_ERROR_CHECK(esp_nimble_hci_and_controller_init());

    nimble_port_init();
    /* Initialize the NimBLE host configuration. */
    ble_hs_cfg.reset_cb = bleprph_on_reset;
    ble_hs_cfg.sync_cb = bleprph_on_sync;
    ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
    ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
    ble_hs_cfg.sm_io_cap = 3;
    ble_hs_cfg.sm_sc = 0;

    rc = gatt_svr_init();
    assert(rc == 0);

    /* Set the default device name. */
    rc = ble_svc_gap_device_name_set("RE:GEN");
    assert(rc == 0);

    ftms_init();
    // stack size determined by uxTaskGetStackHighWaterMark on version "V18.19-2"
    xTaskCreate(bluetooth_user_task, "bluetooth_user_task", 3072, NULL, 5, NULL);

    nimble_port_freertos_init(bleprph_host_task);
}

Private Functions

bleprph_on_reset

handles the NimBLE stack reset event. currently just prints a message to the console.

static void bleprph_on_reset(int reason)
{
    MODLOG_DFLT(ERROR, "Resetting state; reason=%d\n", reason);
}

bleprph_on_sync

handles the NimBLE stack sync event. prints device address, then (re)starts advertising.

static void bleprph_on_sync(void)
{
    int rc;

    rc = ble_hs_util_ensure_addr(0);
    assert(rc == 0);

    /* Figure out address to use while advertising (no privacy for now) */
    rc = ble_hs_id_infer_auto(0, &own_addr_type);
    if (rc != 0)
    {
        MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
        return;
    }

    /* Printing ADDR */
    uint8_t addr_val[6] = {0};
    rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);

    MODLOG_DFLT(INFO, "Device Address: ");
    print_addr(tag, addr_val);
    MODLOG_DFLT(INFO, "\n");
    /* Begin advertising. */
    bleprph_advertise();
}

bleprph_host_task

boilerplate to start the NimBLE host task.

static void bleprph_host_task(void *param)
{
    ESP_LOGI(tag, "BLE Host Task Started");
    /* This function will return only when nimble_port_stop() is executed */
    nimble_port_run();

    nimble_port_freertos_deinit();
}

bluetooth_user_task

handles sending bluetooth messages on a schedule, currently just calls ftms_tick in future more tasks may be added, and we will need a scheduler here.

static void bluetooth_user_task(void *param)
{
    ESP_LOGI(tag, "BLE User Task Started");
    while(1)
    {
        ftms_tick();
        vTaskDelay(330 / portTICK_PERIOD_MS);
    }
}

gap.c

Configuration

VERBOSE_GAP_LOGGING

if set to 1, GAP task will log all gap events to the console.

#define VERBOSE_GAP_LOGGING 0

Public Types

subscription_t

a struct that holds the information for a devices subscription status. used to only send notifications to devices that are subscribed.

typedef struct {
    bool status_subscription;
    bool indoor_bike_subscription;
} subscriptions_t;

connected_device_mac_t

a struct that holds the information for a connected device.

typedef struct {
    uint8_t mac[6];
} connected_device_mac_t;

Public Variables

subscriptions

an array of subscription_t structs, one for each possible connection.

subscriptions_t subscriptions[CONFIG_BT_NIMBLE_MAX_CONNECTIONS] = {0};

connected_devices

an array of connected_device_mac_t structs, one for each possible connection. if a device is connected, the mac address will be stored here. if it is not connected, the mac address will be all 0's.

connected_device_mac_t connected_devices[CONFIG_BT_NIMBLE_MAX_CONNECTIONS] = {0};

Public Functions

bleprph_advertise

starts the BLE advertising process. generaly we should allways be advertising, as we support multiple connections.

/**
 * Enables advertising with the following parameters:
 *     o General discoverable mode.
 *     o Undirected connectable mode.
 */
void bleprph_advertise(void)
{
    struct ble_gap_adv_params adv_params;
    struct ble_hs_adv_fields fields;
    const char *name;
    int rc;

    /**
     *  Set the advertisement data included in our advertisements:
     *     o Flags (indicates advertisement type and other general info).
     *     o Advertising tx power.
     *     o Device name.
     *     o 16-bit service UUIDs (FTMS).
     */

    memset(&fields, 0, sizeof fields);

    /* Advertise two flags:
     *     o Discoverability in forthcoming advertisement (general)
     *     o BLE-only (BR/EDR unsupported).
     */
    fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;

    /* Indicate that the TX power level field should be included; have the
     * stack fill this value automatically.  This is done by assigning the
     * special value BLE_HS_ADV_TX_PWR_LVL_AUTO.
     */
    fields.tx_pwr_lvl_is_present = 1;
    fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;

    name = ble_svc_gap_device_name();
    fields.name = (uint8_t *)name;
    fields.name_len = strlen(name);
    fields.name_is_complete = 1;

    fields.uuids16 = (ble_uuid16_t[]){
        BLE_UUID16_INIT(FTMS_UUID_SERVICE),
        BLE_UUID16_INIT(OTA_SERVICE_UUID),
        BLE_UUID16_INIT(EFTMS_UUID_SERVICE)
        };
    fields.num_uuids16 = 2;
    fields.uuids16_is_complete = 1;
    fields.appearance = ble_svc_gap_device_appearance();
    fields.appearance_is_present = 1;

    rc = ble_gap_adv_set_fields(&fields);
    if (rc != 0)
    {
        MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc);
        return;
    }

    /* Begin advertising. */
    memset(&adv_params, 0, sizeof adv_params);
    adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
    adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
    rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params, bleprph_gap_event, NULL);
    if (rc != 0)
    {
        MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc);
        return;
    }
}

bleprph_gap_event

handles the GAP events.

on connection, it prints the mac address of the connected device, and adds it to the connected_devices array, then restarts advertising.

on disconnection, it removes the device from the connected_devices array, and clears any of its subscriptions.

on advertising end, it restarts advertising.

on subscription, it sets the subscription status for the device.

int bleprph_gap_event(struct ble_gap_event *event, void *arg)
{
    struct ble_gap_conn_desc desc;
    int rc;

    switch (event->type)
    {
        case BLE_GAP_EVENT_CONNECT:
            /* A new connection was established or a connection attempt failed. */
            GAP_LOG_VERBOSE("connection %s; status=%d ", event->connect.status == 0 ? "established" : "failed", event->connect.status);
            if (event->connect.status == 0)
            {
                rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
                assert(rc == 0);
                bleprph_print_conn_desc(&desc);

                // store the connected device's mac address
                memcpy(connected_devices[event->connect.conn_handle].mac, desc.peer_id_addr.val, 6);
            }
            GAP_LOG_VERBOSE("\n");

            // multiple connections should be supported
            // restart advertising
            bleprph_advertise();
            return 0;

        case BLE_GAP_EVENT_DISCONNECT:
            GAP_LOG_VERBOSE("disconnect; reason=%d ", event->disconnect.reason);
            bleprph_print_conn_desc(&event->disconnect.conn);
            subscriptions[event->disconnect.conn.conn_handle].status_subscription = false;
            subscriptions[event->disconnect.conn.conn_handle].indoor_bike_subscription = false;
            // clear the connected device's mac address
            memset(connected_devices[event->disconnect.conn.conn_handle].mac, 0, 6);
            GAP_LOG_VERBOSE("\n");
            return 0;

        case BLE_GAP_EVENT_CONN_UPDATE:
            /* The central has updated the connection parameters. */
            GAP_LOG_VERBOSE("connection updated; status=%d ", event->conn_update.status);
            rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
            assert(rc == 0);
            bleprph_print_conn_desc(&desc);
            GAP_LOG_VERBOSE("\n");
            return 0;

        case BLE_GAP_EVENT_ADV_COMPLETE:
            GAP_LOG_VERBOSE("advertise complete; reason=%d", event->adv_complete.reason);
            bleprph_advertise();
            return 0;

        case BLE_GAP_EVENT_ENC_CHANGE:
            /* Encryption has been enabled or disabled for this connection. */
            GAP_LOG_VERBOSE("encryption change event; status=%d ", event->enc_change.status);
            rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc);
            assert(rc == 0);
            bleprph_print_conn_desc(&desc);
            GAP_LOG_VERBOSE("\n");
            return 0;

        case BLE_GAP_EVENT_NOTIFY_TX:
            GAP_LOG_VERBOSE(
                "notify_tx event; conn_handle=%d attr_handle=%d "
                "status=%d is_indication=%d",
                event->notify_tx.conn_handle, event->notify_tx.attr_handle, event->notify_tx.status, event->notify_tx.indication);
            return 0;

        case BLE_GAP_EVENT_SUBSCRIBE:
            GAP_LOG_VERBOSE(
                "subscribe event; conn_handle=%d attr_handle=%d "
                "reason=%d prevn=%d curn=%d previ=%d curi=%d\n",
                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);

            if (event->subscribe.attr_handle == status_handle)
            {
                GAP_LOG_VERBOSE("status subscription: %d\n", event->subscribe.cur_notify);
                subscriptions[event->subscribe.conn_handle].status_subscription = event->subscribe.cur_notify;
            }
            else if (event->subscribe.attr_handle == indoor_bike_data_handle)
            {
                GAP_LOG_VERBOSE("indoor bike subscription: %d\n", event->subscribe.cur_notify);
                subscriptions[event->subscribe.conn_handle].indoor_bike_subscription = event->subscribe.cur_notify;
            }

            return 0;

        case BLE_GAP_EVENT_MTU:
            GAP_LOG_VERBOSE("mtu update event; conn_handle=%d cid=%d mtu=%d\n", event->mtu.conn_handle, event->mtu.channel_id, event->mtu.value);
            return 0;

        case BLE_GAP_EVENT_REPEAT_PAIRING:
            /* We already have a bond with the peer, but it is attempting to
             * establish a new secure link.  This app sacrifices security for
             * convenience: just throw away the old bond and accept the new link.
             */

            /* Delete the old bond. */
            rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
            assert(rc == 0);
            ble_store_util_delete_peer(&desc.peer_id_addr);

            /* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
             * continue with the pairing operation.
             */
            return BLE_GAP_REPEAT_PAIRING_RETRY;
    }

    return 0;
}

Private Functions

bleprph_print_conn_desc

prints the connection description to the console.

static void bleprph_print_conn_desc(struct ble_gap_conn_desc *desc)
{
    if(!VERBOSE_GAP_LOGGING)
    {
        return;
    }
    GAP_LOG_VERBOSE("handle=%d our_ota_addr_type=%d our_ota_addr=", desc->conn_handle, desc->our_ota_addr.type);
    GAP_LOG_ADDRESS(desc->our_ota_addr.val);
    GAP_LOG_VERBOSE(" our_id_addr_type=%d our_id_addr=", desc->our_id_addr.type);
    GAP_LOG_ADDRESS(desc->our_id_addr.val);
    GAP_LOG_VERBOSE(" peer_ota_addr_type=%d peer_ota_addr=", desc->peer_ota_addr.type);
    GAP_LOG_ADDRESS(desc->peer_ota_addr.val);
    GAP_LOG_VERBOSE(" peer_id_addr_type=%d peer_id_addr=", desc->peer_id_addr.type);
    GAP_LOG_ADDRESS(desc->peer_id_addr.val);
    GAP_LOG_VERBOSE(
        " conn_itvl=%d conn_latency=%d supervision_timeout=%d "
        "encrypted=%d authenticated=%d bonded=%d\n",
        desc->conn_itvl, desc->conn_latency, desc->supervision_timeout, desc->sec_state.encrypted, desc->sec_state.authenticated, desc->sec_state.bonded);
}

gatt.c

Public Variables

*_handle

the handles for the BLE services, characteristics, and descriptors. set to 0, as they will be set by the GATT server.

uint16_t control_point_handle = 0;
uint16_t status_handle = 0;
uint16_t feature_handle = 0;
uint16_t indoor_bike_data_handle = 0;
uint16_t support_resistance_handle = 0;
uint16_t support_power_handle = 0;
uint16_t ota_handle = 0;
uint16_t eftms_handle = 0;

Public Functions

gatt_svr_register_cb

required callback for the nimble stack. currently just prints a message to the console.

void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
{
    char buf[BLE_UUID_STR_LEN];

    switch (ctxt->op)
    {
        case BLE_GATT_REGISTER_OP_SVC:
            MODLOG_DFLT(DEBUG, "registered service %s with handle=%d\n", ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle);
            break;

        case BLE_GATT_REGISTER_OP_CHR:
            MODLOG_DFLT(DEBUG,
                        "registering characteristic %s with "
                        "def_handle=%d val_handle=%d\n",
                        ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), ctxt->chr.def_handle, ctxt->chr.val_handle);
            break;

        case BLE_GATT_REGISTER_OP_DSC:
            MODLOG_DFLT(DEBUG, "registering descriptor %s with handle=%d\n", ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle);
            break;

        default:
            assert(0);
            break;
    }
}

gatt_svr_init

initializes the GATT server, and sets up the BLE services, characteristics, and descriptors.

int gatt_svr_init(void)
{
    int rc;

    ble_svc_gap_init();
    ble_svc_gatt_init();
    ble_svc_ans_init();

    rc = ble_gatts_count_cfg(gatt_svr_svcs);
    if (rc != 0)
    {
        return rc;
    }

    rc = ble_gatts_add_svcs(gatt_svr_svcs);
    if (rc != 0)
    {
        return rc;
    }

    return 0;
}

Private Variables

gatt_svr_svcs

an array of BLE services, characteristics, and descriptors.

this defines the bikes BLE interface to the outside world.

it currently consists of 3 services:

  • the Fitness Machine Service (FTMS)
  • the EFTMS Service (Extended / Energym FTMS)
  • the OTA Service (Over The Air updates)

more documentation on these services can be found in their respective handlers.

as a note to how this works, these lists are all terminated by an entry (service or characteristic) with 0 in all fields.

// clang-format off
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(FTMS_UUID_SERVICE),
        .characteristics = (struct ble_gatt_chr_def[]){
            {
                .uuid = BLE_UUID16_DECLARE(FTMS_UUID_CHAR_FEATURE),
                .access_cb = dummy_handler,
                .flags = BLE_GATT_CHR_F_READ,
                .val_handle = &feature_handle,
            },
            {
                .uuid = BLE_UUID16_DECLARE(FTMS_UUID_CHAR_STATUS),
                .access_cb = dummy_handler,
                .flags = BLE_GATT_CHR_F_NOTIFY,
                .val_handle = &status_handle,
            },
            {
                .uuid = BLE_UUID16_DECLARE(FTMS_UUID_CHAR_CONTROL_POINT),
                .access_cb = ftms_control_point_handler,
                .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE,
                .val_handle = &control_point_handle,
            },
            {
                .uuid = BLE_UUID16_DECLARE(INDOOR_BIKE_DATA_CHAR_UUID),
                .access_cb = dummy_handler,
                .flags = BLE_GATT_CHR_F_NOTIFY,
                .val_handle = &indoor_bike_data_handle,
            },
            // {
            //     .uuid = BLE_UUID16_DECLARE(FTMS_UUID_SUPPORTED_INCLINATION_RANGE),
            //     .access_cb = dummy_handler,
            //     .flags = BLE_GATT_CHR_F_READ,
            // },
            {
                .uuid = BLE_UUID16_DECLARE(FTMS_UUID_SUPPORTED_RESISTANCE_LEVEL_RANGE),
                .access_cb = dummy_handler,
                .flags = BLE_GATT_CHR_F_READ,
                .val_handle = &support_resistance_handle,
            },
            // {
            //     .uuid = BLE_UUID16_DECLARE(FTMS_UUID_SUPPORTED_POWER_RANGE),
            //     .access_cb = dummy_handler,
            //     .flags = BLE_GATT_CHR_F_READ,
            //     .val_handle = &support_power_handle,
            // },
            {
                0, /* No more characteristics in this service. */
            },
        },
    },
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(OTA_SERVICE_UUID),
        .characteristics = (struct ble_gatt_chr_def[]){
            {
                .uuid = BLE_UUID16_DECLARE(OTA_CHAR_UUID),
                .access_cb = ota_handler,
                .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
                .val_handle = &ota_handle,
            },
            {
                0, /* No more characteristics in this service. */
            },
        },
    },
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(EFTMS_UUID_SERVICE),
        .characteristics = (struct ble_gatt_chr_def[]){
            {
                .uuid = BLE_UUID16_DECLARE(EFTMS_UUID_CHAR),
                .access_cb = eftms_handler,
                .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
                .val_handle = &eftms_handle,
            },
            {
                0, /* No more characteristics in this service. */
            },
        },
    },
    {
        0, /* No more services. */
    },
};

Private Functions

dummy_handler

a dummy handler that does nothing. used to prevent the NimBLE stack from crashing if a characteristic is interacted with that has no handler.

static int dummy_handler(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    return 0;
}