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;
}