From bc83d470e37453d1694593fe372e321d3e6dcd85 Mon Sep 17 00:00:00 2001 From: Anurag Kar Date: Fri, 10 May 2019 03:00:13 +0530 Subject: [PATCH 1/3] protocomm_ble : Fixed custom service UUID support List of changes: * Use 128 bit characteristic UUIDs when creating GATT table entries * Change primary service attribute value to 128 bit custom service UUID * Use raw advertisement data to convey flags and 128 bit primary service UUID * Use raw scan response to send device name as complete local name * Increase maximum device name length in relation to maximum scan response length * Set Characteristic User Description attributes for each characteristic to convey protocomm endpoint names --- .../include/transports/protocomm_ble.h | 17 +- .../protocomm/src/simple_ble/simple_ble.c | 41 ++- .../protocomm/src/simple_ble/simple_ble.h | 18 +- .../protocomm/src/transports/protocomm_ble.c | 261 +++++++++++++----- 4 files changed, 253 insertions(+), 84 deletions(-) diff --git a/components/protocomm/include/transports/protocomm_ble.h b/components/protocomm/include/transports/protocomm_ble.h index 562ea608c..92714444f 100644 --- a/components/protocomm/include/transports/protocomm_ble.h +++ b/components/protocomm/include/transports/protocomm_ble.h @@ -14,6 +14,8 @@ #pragma once +#include + #include #ifdef __cplusplus @@ -22,8 +24,9 @@ extern "C" { /** * BLE device name cannot be larger than this value + * 31 bytes (max scan response size) - 1 byte (length) - 1 byte (type) = 29 bytes */ -#define MAX_BLE_DEVNAME_LEN 13 +#define MAX_BLE_DEVNAME_LEN (ESP_BLE_SCAN_RSP_DATA_LEN_MAX - 2) /** * @brief This structure maps handler required by protocomm layer to @@ -51,8 +54,16 @@ typedef struct { * BLE device name being broadcast at the time of provisioning */ char device_name[MAX_BLE_DEVNAME_LEN]; - uint8_t service_uuid[16]; /*!< SSID of the provisioning service */ - ssize_t nu_lookup_count; /*!< Number of entries in the Name-UUID lookup table */ + + /** + * 128 bit UUID of the provisioning service + */ + uint8_t service_uuid[ESP_UUID_LEN_128]; + + /** + * Number of entries in the Name-UUID lookup table + */ + ssize_t nu_lookup_count; /** * Pointer to the Name-UUID lookup table diff --git a/components/protocomm/src/simple_ble/simple_ble.c b/components/protocomm/src/simple_ble/simple_ble.c index 61f65021b..ee5091168 100644 --- a/components/protocomm/src/simple_ble/simple_ble.c +++ b/components/protocomm/src/simple_ble/simple_ble.c @@ -30,25 +30,37 @@ static const char *TAG = "simple_ble"; static simple_ble_cfg_t *g_ble_cfg_p; static uint16_t g_gatt_table_map[SIMPLE_BLE_MAX_GATT_TABLE_SIZE]; -uint16_t simple_ble_get_uuid(uint16_t handle) +static uint8_t adv_config_done; +#define adv_config_flag (1 << 0) +#define scan_rsp_config_flag (1 << 1) + +const uint8_t *simple_ble_get_uuid128(uint16_t handle) { - uint16_t *uuid_ptr; + const uint8_t *uuid128_ptr; for (int i = 0; i < SIMPLE_BLE_MAX_GATT_TABLE_SIZE; i++) { if (g_gatt_table_map[i] == handle) { - uuid_ptr = (uint16_t *) g_ble_cfg_p->gatt_db[i].att_desc.uuid_p; - return *uuid_ptr; + uuid128_ptr = (const uint8_t *) g_ble_cfg_p->gatt_db[i].att_desc.uuid_p; + return uuid128_ptr; } } - return -1; + return NULL; } static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - esp_ble_gap_start_advertising(&g_ble_cfg_p->adv_params); - + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + adv_config_done &= (~adv_config_flag); + if (adv_config_done == 0) { + esp_ble_gap_start_advertising(&g_ble_cfg_p->adv_params); + } + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: + adv_config_done &= (~scan_rsp_config_flag); + if (adv_config_done == 0) { + esp_ble_gap_start_advertising(&g_ble_cfg_p->adv_params); + } break; default: break; @@ -87,11 +99,20 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ ESP_LOGE(TAG, "set device name failed, error code = 0x%x", ret); return; } - ret = esp_ble_gap_config_adv_data(&g_ble_cfg_p->adv_data); + ret = esp_ble_gap_config_adv_data_raw(g_ble_cfg_p->raw_adv_data_p, + g_ble_cfg_p->raw_adv_data_len); if (ret) { - ESP_LOGE(TAG, "config adv data failed, error code = 0x%x", ret); + ESP_LOGE(TAG, "config raw adv data failed, error code = 0x%x ", ret); return; } + adv_config_done |= adv_config_flag; + ret = esp_ble_gap_config_scan_rsp_data_raw(g_ble_cfg_p->raw_scan_rsp_data_p, + g_ble_cfg_p->raw_scan_rsp_data_len); + if (ret) { + ESP_LOGE(TAG, "config raw scan rsp data failed, error code = 0x%x", ret); + return; + } + adv_config_done |= scan_rsp_config_flag; break; case ESP_GATTS_READ_EVT: g_ble_cfg_p->read_fn(event, gatts_if, param); diff --git a/components/protocomm/src/simple_ble/simple_ble.h b/components/protocomm/src/simple_ble/simple_ble.h index 4402429f6..d4f927b61 100644 --- a/components/protocomm/src/simple_ble/simple_ble.h +++ b/components/protocomm/src/simple_ble/simple_ble.h @@ -32,11 +32,16 @@ typedef void (simple_ble_cb_t)(esp_gatts_cb_event_t event, esp_gatt_if_t p_gatts typedef struct { /** Name to be displayed to devices scanning for ESP32 */ const char *device_name; - /** Advertising data content, according to "Supplement to the Bluetooth Core Specification" */ - esp_ble_adv_data_t adv_data; + /** Raw advertisement data */ + uint8_t *raw_adv_data_p; + uint8_t raw_adv_data_len; + /** Raw scan response data */ + uint8_t *raw_scan_rsp_data_p; + uint8_t raw_scan_rsp_data_len; /** Parameters to configure the nature of advertising */ esp_ble_adv_params_t adv_params; - /** Descriptor table which consists the configuration required by services and characteristics */ + /** Descriptor table which consists of the configuration + * required by services and characteristics */ esp_gatts_attr_db_t *gatt_db; /** Number of entries in the gatt_db descriptor table */ ssize_t gatt_db_count; @@ -94,14 +99,15 @@ esp_err_t simple_ble_start(simple_ble_cfg_t *cfg); */ esp_err_t simple_ble_stop(); -/** Convert handle to UUID of characteristic +/** Convert handle to 128 bit UUID of characteristic * * This function can be easily used to get the corresponding * UUID for a characteristic that has been created, and the one for * which we only have the handle for. * - * @return uuid the UUID of the handle, -1 in case of invalid handle + * @return Pointer to UUID of the characteristic + * NULL in case of invalid handle */ -uint16_t simple_ble_get_uuid(uint16_t handle); +const uint8_t *simple_ble_get_uuid128(uint16_t handle); #endif /* _SIMPLE_BLE_ */ diff --git a/components/protocomm/src/transports/protocomm_ble.c b/components/protocomm/src/transports/protocomm_ble.c index 5e65aa95d..f6b527572 100644 --- a/components/protocomm/src/transports/protocomm_ble.c +++ b/components/protocomm/src/transports/protocomm_ble.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -28,10 +29,17 @@ static const char *TAG = "protocomm_ble"; /* BLE specific configuration parameters */ -const uint16_t GATTS_SERVICE_UUID_PROV = 0xFFFF; -const uint16_t primary_service_uuid = ESP_GATT_UUID_PRI_SERVICE; -const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; -const uint8_t char_prop_read_write = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; +static const uint16_t primary_service_uuid = ESP_GATT_UUID_PRI_SERVICE; +static const uint16_t character_declaration_uuid = ESP_GATT_UUID_CHAR_DECLARE; +static const uint16_t character_user_description = ESP_GATT_UUID_CHAR_DESCRIPTION; +static const uint8_t character_prop_read_write = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; +static const uint8_t ble_advertisement_flags = ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT; + +typedef struct { + uint8_t type; + uint8_t length; + uint8_t *data_p; +} raw_data_info_t; typedef struct { uint8_t *prepare_buf; @@ -41,11 +49,21 @@ typedef struct { static prepare_type_env_t prepare_write_env; +typedef struct name_uuid128 { + const char *name; + uint8_t uuid128[ESP_UUID_LEN_128]; +} name_uuid128_t; + typedef struct _protocomm_ble { protocomm_t *pc_ble; - protocomm_ble_name_uuid_t *g_nu_lookup; + name_uuid128_t *g_nu_lookup; ssize_t g_nu_lookup_count; uint16_t gatt_mtu; + uint8_t *service_uuid; + uint8_t *raw_adv_data_p; + uint8_t raw_adv_data_len; + uint8_t *raw_scan_rsp_data_p; + uint8_t raw_scan_rsp_data_len; } _protocomm_ble_internal_t; static _protocomm_ble_internal_t *protoble_internal; @@ -61,34 +79,25 @@ static esp_ble_adv_params_t adv_params = { static char* protocomm_ble_device_name = NULL; -/* The length of adv data must be less than 31 bytes */ -static esp_ble_adv_data_t adv_data = { - .set_scan_rsp = false, - .include_name = true, - .include_txpower = true, - .min_interval = 0x100, - .max_interval = 0x100, - .appearance = ESP_BLE_APPEARANCE_UNKNOWN, - .manufacturer_len = 0, - .p_manufacturer_data = NULL, - .service_data_len = 0, - .p_service_data = NULL, - .service_uuid_len = 0, - .p_service_uuid = NULL, - .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), -}; - static void hexdump(const char *msg, uint8_t *buf, int len) { ESP_LOGD(TAG, "%s:", msg); ESP_LOG_BUFFER_HEX_LEVEL(TAG, buf, len, ESP_LOG_DEBUG); } +static const uint16_t *uuid128_to_16(const uint8_t *uuid128) +{ + return (const uint16_t *) &uuid128[12]; +} + static const char *handle_to_handler(uint16_t handle) { - uint16_t uuid = simple_ble_get_uuid(handle); + const uint8_t *uuid128 = simple_ble_get_uuid128(handle); + if (!uuid128) { + return NULL; + } for (int i = 0; i < protoble_internal->g_nu_lookup_count; i++) { - if (protoble_internal->g_nu_lookup[i].uuid == uuid ) { + if (*uuid128_to_16(protoble_internal->g_nu_lookup[i].uuid128) == *uuid128_to_16(uuid128)) { return protoble_internal->g_nu_lookup[i].name; } } @@ -226,7 +235,6 @@ static void transport_simple_ble_write(esp_gatts_cb_event_t event, esp_gatt_if_t } } - static void transport_simple_ble_exec_write(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { esp_err_t err; @@ -314,12 +322,17 @@ static esp_err_t protocomm_ble_remove_endpoint(const char *ep_name) return ESP_OK; } - static ssize_t populate_gatt_db(esp_gatts_attr_db_t **gatt_db_generated) { int i; - /* We need esp_gatts_attr_db_t of size 2 * number of handlers + 1 for service */ - ssize_t gatt_db_generated_entries = 2 * protoble_internal->g_nu_lookup_count + 1; + /* Each endpoint requires 3 attributes: + * 1) for Characteristic Declaration + * 2) for Characteristic Value (for reading and writing to an endpoint) + * 3) for Characteristic User Description (endpoint name) + * + * Therefore, we need esp_gatts_attr_db_t of size 3 * number of endpoints + 1 for service + */ + ssize_t gatt_db_generated_entries = 3 * protoble_internal->g_nu_lookup_count + 1; *gatt_db_generated = (esp_gatts_attr_db_t *) malloc(sizeof(esp_gatts_attr_db_t) * (gatt_db_generated_entries)); @@ -333,28 +346,38 @@ static ssize_t populate_gatt_db(esp_gatts_attr_db_t **gatt_db_generated) (*gatt_db_generated)[0].att_desc.uuid_length = ESP_UUID_LEN_16; (*gatt_db_generated)[0].att_desc.uuid_p = (uint8_t *) &primary_service_uuid; (*gatt_db_generated)[0].att_desc.perm = ESP_GATT_PERM_READ; - (*gatt_db_generated)[0].att_desc.max_length = sizeof(uint16_t); - (*gatt_db_generated)[0].att_desc.length = sizeof(GATTS_SERVICE_UUID_PROV); - (*gatt_db_generated)[0].att_desc.value = (uint8_t *) &GATTS_SERVICE_UUID_PROV; + (*gatt_db_generated)[0].att_desc.max_length = ESP_UUID_LEN_128; + (*gatt_db_generated)[0].att_desc.length = ESP_UUID_LEN_128; + (*gatt_db_generated)[0].att_desc.value = protoble_internal->service_uuid; /* Declare characteristics */ for (i = 1 ; i < gatt_db_generated_entries ; i++) { - (*gatt_db_generated)[i].attr_control.auto_rsp = ESP_GATT_RSP_BY_APP; + (*gatt_db_generated)[i].attr_control.auto_rsp = ESP_GATT_RSP_BY_APP; - (*gatt_db_generated)[i].att_desc.uuid_length = ESP_UUID_LEN_16; - (*gatt_db_generated)[i].att_desc.perm = ESP_GATT_PERM_READ | - ESP_GATT_PERM_WRITE; - - if (i % 2 == 1) { /* Char Declaration */ - (*gatt_db_generated)[i].att_desc.uuid_p = (uint8_t *) &character_declaration_uuid; - (*gatt_db_generated)[i].att_desc.max_length = sizeof(uint8_t); - (*gatt_db_generated)[i].att_desc.length = sizeof(uint8_t); - (*gatt_db_generated)[i].att_desc.value = (uint8_t *) &char_prop_read_write; - } else { /* Char Value */ - (*gatt_db_generated)[i].att_desc.uuid_p = (uint8_t *)&protoble_internal->g_nu_lookup[i / 2 - 1].uuid; - (*gatt_db_generated)[i].att_desc.max_length = CHAR_VAL_LEN_MAX; - (*gatt_db_generated)[i].att_desc.length = 0; - (*gatt_db_generated)[i].att_desc.value = NULL; + if (i % 3 == 1) { + /* Characteristic Declaration */ + (*gatt_db_generated)[i].att_desc.perm = ESP_GATT_PERM_READ; + (*gatt_db_generated)[i].att_desc.uuid_length = ESP_UUID_LEN_16; + (*gatt_db_generated)[i].att_desc.uuid_p = (uint8_t *) &character_declaration_uuid; + (*gatt_db_generated)[i].att_desc.max_length = sizeof(uint8_t); + (*gatt_db_generated)[i].att_desc.length = sizeof(uint8_t); + (*gatt_db_generated)[i].att_desc.value = (uint8_t *) &character_prop_read_write; + } else if (i % 3 == 2) { + /* Characteristic Value */ + (*gatt_db_generated)[i].att_desc.perm = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + (*gatt_db_generated)[i].att_desc.uuid_length = ESP_UUID_LEN_128; + (*gatt_db_generated)[i].att_desc.uuid_p = protoble_internal->g_nu_lookup[i / 3].uuid128; + (*gatt_db_generated)[i].att_desc.max_length = CHAR_VAL_LEN_MAX; + (*gatt_db_generated)[i].att_desc.length = 0; + (*gatt_db_generated)[i].att_desc.value = NULL; + } else { + /* Characteristic User Description (for keeping endpoint names) */ + (*gatt_db_generated)[i].att_desc.perm = ESP_GATT_PERM_READ; + (*gatt_db_generated)[i].att_desc.uuid_length = ESP_UUID_LEN_16; + (*gatt_db_generated)[i].att_desc.uuid_p = (uint8_t *) &character_user_description; + (*gatt_db_generated)[i].att_desc.max_length = strlen(protoble_internal->g_nu_lookup[i / 3 - 1].name); + (*gatt_db_generated)[i].att_desc.length = (*gatt_db_generated)[i].att_desc.max_length; + (*gatt_db_generated)[i].att_desc.value = (uint8_t *) protoble_internal->g_nu_lookup[i / 3 - 1].name; } } return gatt_db_generated_entries; @@ -371,6 +394,8 @@ static void protocomm_ble_cleanup(void) } free(protoble_internal->g_nu_lookup); } + free(protoble_internal->raw_adv_data_p); + free(protoble_internal->raw_scan_rsp_data_p); free(protoble_internal); protoble_internal = NULL; } @@ -378,11 +403,6 @@ static void protocomm_ble_cleanup(void) free(protocomm_ble_device_name); protocomm_ble_device_name = NULL; } - if (adv_data.p_service_uuid) { - free(adv_data.p_service_uuid); - adv_data.p_service_uuid = NULL; - adv_data.service_uuid_len = 0; - } } esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *config) @@ -396,16 +416,6 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con return ESP_FAIL; } - /* Store service UUID internally */ - adv_data.service_uuid_len = sizeof(config->service_uuid); - adv_data.p_service_uuid = malloc(sizeof(config->service_uuid)); - if (adv_data.p_service_uuid == NULL) { - ESP_LOGE(TAG, "Error allocating memory for storing service UUID"); - protocomm_ble_cleanup(); - return ESP_ERR_NO_MEM; - } - memcpy(adv_data.p_service_uuid, config->service_uuid, adv_data.service_uuid_len); - /* Store BLE device name internally */ protocomm_ble_device_name = strdup(config->device_name); if (protocomm_ble_device_name == NULL) { @@ -421,8 +431,8 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con return ESP_ERR_NO_MEM; } - protoble_internal->g_nu_lookup_count = config->nu_lookup_count; - protoble_internal->g_nu_lookup = malloc(config->nu_lookup_count * sizeof(protocomm_ble_name_uuid_t)); + protoble_internal->g_nu_lookup_count = config->nu_lookup_count; + protoble_internal->g_nu_lookup = malloc(config->nu_lookup_count * sizeof(name_uuid128_t)); if (protoble_internal->g_nu_lookup == NULL) { ESP_LOGE(TAG, "Error allocating internal name UUID table"); protocomm_ble_cleanup(); @@ -430,7 +440,10 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con } for (unsigned i = 0; i < protoble_internal->g_nu_lookup_count; i++) { - protoble_internal->g_nu_lookup[i].uuid = config->nu_lookup[i].uuid; + memcpy(protoble_internal->g_nu_lookup[i].uuid128, config->service_uuid, ESP_UUID_LEN_128); + memcpy((uint8_t *)uuid128_to_16(protoble_internal->g_nu_lookup[i].uuid128), + &config->nu_lookup[i].uuid, ESP_UUID_LEN_16); + protoble_internal->g_nu_lookup[i].name = strdup(config->nu_lookup[i].name); if (protoble_internal->g_nu_lookup[i].name == NULL) { ESP_LOGE(TAG, "Error allocating internal name UUID entry"); @@ -444,6 +457,120 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con protoble_internal->pc_ble = pc; protoble_internal->gatt_mtu = ESP_GATT_DEF_BLE_MTU_SIZE; + /* The BLE advertisement data (max 31 bytes) consists of: + * 1) Flags - + * Size : length (1 byte) + type (1 byte) + value (1 byte) = 3 bytes + * 2) Complete 128 bit UUID of the service - + * Size : length (1 byte) + type (1 byte) + value (16 bytes) = 18 bytes + * + * Remaining 31 - (3 + 18) = 10 bytes could be used for manufacturer data + * or something else in the future. + */ + raw_data_info_t adv_data[] = { + { /* Flags */ + .type = ESP_BLE_AD_TYPE_FLAG, + .length = sizeof(ble_advertisement_flags), + .data_p = (uint8_t *) &ble_advertisement_flags + }, + { /* 128 bit Service UUID */ + .type = ESP_BLE_AD_TYPE_128SRV_CMPL, + .length = ESP_UUID_LEN_128, + .data_p = (uint8_t *) config->service_uuid + }, + }; + + /* Get the total raw data length required for above entries */ + uint8_t adv_data_len = 0; + for (uint8_t i = 0; i < (sizeof(adv_data)/sizeof(adv_data[0])); i++) { + /* Add extra bytes required per entry, i.e. + * length (1 byte) + type (1 byte) = 2 bytes */ + adv_data_len += adv_data[i].length + 2; + } + if (adv_data_len > ESP_BLE_ADV_DATA_LEN_MAX) { + ESP_LOGE(TAG, "Advertisement data too long = %d bytes", adv_data_len); + protocomm_ble_cleanup(); + return ESP_ERR_NO_MEM; + } + + /* Allocate memory for the raw advertisement data */ + protoble_internal->raw_adv_data_len = adv_data_len; + protoble_internal->raw_adv_data_p = malloc(adv_data_len); + if (protoble_internal->raw_adv_data_p == NULL) { + ESP_LOGE(TAG, "Error allocating memory for raw advertisement data"); + protocomm_ble_cleanup(); + return ESP_ERR_NO_MEM; + } + + /* Form the raw advertisement data using above entries */ + for (uint8_t i = 0, len = 0; i < (sizeof(adv_data)/sizeof(adv_data[0])); i++) { + protoble_internal->raw_adv_data_p[len++] = adv_data[i].length + 1; // + 1 byte for type + protoble_internal->raw_adv_data_p[len++] = adv_data[i].type; + memcpy(&protoble_internal->raw_adv_data_p[len], + adv_data[i].data_p, adv_data[i].length); + + if (adv_data[i].type == ESP_BLE_AD_TYPE_128SRV_CMPL) { + /* Remember where the primary service UUID is kept in the + * raw advertisement data, so that it can be used while + * populating the GATT database + */ + protoble_internal->service_uuid = &protoble_internal->raw_adv_data_p[len]; + } + + len += adv_data[i].length; + } + + size_t ble_devname_len = strlen(protocomm_ble_device_name); + /* The BLE scan response (31 bytes) consists of: + * 1) Device name (complete / incomplete) - + * Size : The maximum supported name length + * will be 31 - 2 (length + type) = 29 bytes + * + * Any remaining space may be used for accommodating + * other fields in the future + */ + raw_data_info_t scan_resp_data[] = { + { /* If full device name can fit in the scan response then indicate + * that by setting type to "Complete Name", else set it to "Short Name" + * so that client can fetch full device name - after connecting - by + * reading the device name characteristic under GAP service */ + .type = (ble_devname_len > (ESP_BLE_SCAN_RSP_DATA_LEN_MAX - 2) ? + ESP_BLE_AD_TYPE_NAME_SHORT : ESP_BLE_AD_TYPE_NAME_CMPL), + .length = MIN(ble_devname_len, (ESP_BLE_SCAN_RSP_DATA_LEN_MAX - 2)), + .data_p = (uint8_t *) protocomm_ble_device_name + }, + }; + + /* Get the total raw scan response data length required for above entries */ + uint8_t scan_resp_data_len = 0; + for (int i = 0; i < (sizeof(scan_resp_data)/sizeof(scan_resp_data[0])); i++) { + /* Add extra bytes required per entry, i.e. + * length (1 byte) + type (1 byte) = 2 bytes */ + scan_resp_data_len += scan_resp_data[i].length + 2; + } + if (scan_resp_data_len > ESP_BLE_SCAN_RSP_DATA_LEN_MAX) { + ESP_LOGE(TAG, "Scan response data too long = %d bytes", scan_resp_data_len); + protocomm_ble_cleanup(); + return ESP_ERR_NO_MEM; + } + + /* Allocate memory for the raw scan response data */ + protoble_internal->raw_scan_rsp_data_len = scan_resp_data_len; + protoble_internal->raw_scan_rsp_data_p = malloc(scan_resp_data_len); + if (protoble_internal->raw_scan_rsp_data_p == NULL) { + ESP_LOGE(TAG, "Error allocating memory for raw response data"); + protocomm_ble_cleanup(); + return ESP_ERR_NO_MEM; + } + + /* Form the raw scan response data using above entries */ + for (uint8_t i = 0, len = 0; i < (sizeof(scan_resp_data)/sizeof(scan_resp_data[0])); i++) { + protoble_internal->raw_scan_rsp_data_p[len++] = scan_resp_data[i].length + 1; // + 1 byte for type + protoble_internal->raw_scan_rsp_data_p[len++] = scan_resp_data[i].type; + memcpy(&protoble_internal->raw_scan_rsp_data_p[len], + scan_resp_data[i].data_p, scan_resp_data[i].length); + len += scan_resp_data[i].length; + } + simple_ble_cfg_t *ble_config = simple_ble_init(); if (ble_config == NULL) { ESP_LOGE(TAG, "Ran out of memory for BLE config"); @@ -460,9 +587,13 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con ble_config->set_mtu_fn = transport_simple_ble_set_mtu; /* Set parameters required for advertising */ - ble_config->adv_data = adv_data; ble_config->adv_params = adv_params; + ble_config->raw_adv_data_p = protoble_internal->raw_adv_data_p; + ble_config->raw_adv_data_len = protoble_internal->raw_adv_data_len; + ble_config->raw_scan_rsp_data_p = protoble_internal->raw_scan_rsp_data_p; + ble_config->raw_scan_rsp_data_len = protoble_internal->raw_scan_rsp_data_len; + ble_config->device_name = protocomm_ble_device_name; ble_config->gatt_db_count = populate_gatt_db(&ble_config->gatt_db); From 56866567ae69c84e63d97ce6188bf83e3bdfb0df Mon Sep 17 00:00:00 2001 From: Anurag Kar Date: Fri, 10 May 2019 03:06:56 +0530 Subject: [PATCH 2/3] esp_prov : Runtime discovery of Service UUID and endpoint name mapping List of changes: * Retrieve UUID property from Bluez device object before connecting to retrieve UUID contained in advertisement * Read Characteristic User Descriptions attribute of each UUID for mapping endpoint names * To support older implementations with hardcoded Name-UUID map, revert to fallback mode in order if advertisement data has no UUID field --- tools/esp_prov/esp_prov.py | 7 ++ tools/esp_prov/transport/ble_cli.py | 128 +++++++++++++++++----- tools/esp_prov/transport/transport_ble.py | 21 +++- 3 files changed, 122 insertions(+), 34 deletions(-) diff --git a/tools/esp_prov/esp_prov.py b/tools/esp_prov/esp_prov.py index 21d15e084..88b58c1bb 100644 --- a/tools/esp_prov/esp_prov.py +++ b/tools/esp_prov/esp_prov.py @@ -60,6 +60,13 @@ def get_transport(sel_transport, softap_endpoint=None, ble_devname=None): if (sel_transport == 'softap'): tp = transport.Transport_Softap(softap_endpoint) elif (sel_transport == 'ble'): + # BLE client is now capable of automatically figuring out + # the primary service from the advertisement data and the + # characteristics corresponding to each endpoint. + # Below, the service_uuid field and 16bit UUIDs in the nu_lookup + # table are provided only to support devices running older firmware, + # in which case, the automated discovery will fail and the client + # will fallback to using the provided UUIDs instead tp = transport.Transport_BLE(devname=ble_devname, service_uuid='0000ffff-0000-1000-8000-00805f9b34fb', nu_lookup={'prov-session': 'ff51', diff --git a/tools/esp_prov/transport/ble_cli.py b/tools/esp_prov/transport/ble_cli.py index 04d2921ac..ad80bf2dd 100644 --- a/tools/esp_prov/transport/ble_cli.py +++ b/tools/esp_prov/transport/ble_cli.py @@ -15,6 +15,7 @@ from __future__ import print_function from builtins import input +from future.utils import iteritems import platform @@ -40,20 +41,24 @@ if platform.system() == 'Linux': # BLE client (Linux Only) using Bluez and DBus class BLE_Bluez_Client: - def connect(self, devname, iface, srv_uuid): + def connect(self, devname, iface, chrc_names, fallback_srv_uuid): self.devname = devname - self.srv_uuid = srv_uuid + self.srv_uuid_fallback = fallback_srv_uuid + self.chrc_names = [name.lower() for name in chrc_names] self.device = None self.adapter = None self.adapter_props = None self.services = None + self.nu_lookup = None + self.characteristics = dict() + self.srv_uuid_adv = None dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") objects = manager.GetManagedObjects() - for path, interfaces in objects.items(): + for path, interfaces in iteritems(objects): adapter = interfaces.get("org.bluez.Adapter1") if adapter is not None: if path.endswith(iface): @@ -94,8 +99,8 @@ class BLE_Bluez_Client: manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") objects = manager.GetManagedObjects() dev_path = None - for path, interfaces in objects.items(): - if "org.bluez.Device1" not in interfaces.keys(): + for path, interfaces in iteritems(objects): + if "org.bluez.Device1" not in interfaces: continue if interfaces["org.bluez.Device1"].get("Name") == self.devname: dev_path = path @@ -106,6 +111,19 @@ class BLE_Bluez_Client: try: self.device = bus.get_object("org.bluez", dev_path) + try: + uuids = self.device.Get('org.bluez.Device1', 'UUIDs', + dbus_interface='org.freedesktop.DBus.Properties') + # There should be 1 service UUID in advertising data + # If bluez had cached an old version of the advertisement data + # the list of uuids may be incorrect, in which case connection + # or service discovery may fail the first time. If that happens + # the cache will be refreshed before next retry + if len(uuids) == 1: + self.srv_uuid_adv = uuids[0] + except dbus.exceptions.DBusException as e: + print(e) + self.device.Connect(dbus_interface='org.bluez.Device1') except Exception as e: print(e) @@ -116,35 +134,84 @@ class BLE_Bluez_Client: bus = dbus.SystemBus() manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") objects = manager.GetManagedObjects() - srv_path = None - for path, interfaces in objects.items(): - if "org.bluez.GattService1" not in interfaces.keys(): + service_found = False + for srv_path, srv_interfaces in iteritems(objects): + if "org.bluez.GattService1" not in srv_interfaces: continue - if path.startswith(self.device.object_path): - service = bus.get_object("org.bluez", path) - uuid = service.Get('org.bluez.GattService1', 'UUID', + if not srv_path.startswith(self.device.object_path): + continue + service = bus.get_object("org.bluez", srv_path) + srv_uuid = service.Get('org.bluez.GattService1', 'UUID', dbus_interface='org.freedesktop.DBus.Properties') - if uuid == self.srv_uuid: - srv_path = path - break - if srv_path is None: - self.device.Disconnect(dbus_interface='org.bluez.Device1') - self.device = None - raise RuntimeError("Provisioning service not found") - - self.characteristics = dict() - for path, interfaces in objects.items(): - if "org.bluez.GattCharacteristic1" not in interfaces.keys(): + # If service UUID doesn't match the one found in advertisement data + # then also check if it matches the fallback UUID + if srv_uuid not in [self.srv_uuid_adv, self.srv_uuid_fallback]: continue - if path.startswith(srv_path): - chrc = bus.get_object("org.bluez", path) + + nu_lookup = dict() + characteristics = dict() + for chrc_path, chrc_interfaces in iteritems(objects): + if "org.bluez.GattCharacteristic1" not in chrc_interfaces: + continue + if not chrc_path.startswith(service.object_path): + continue + chrc = bus.get_object("org.bluez", chrc_path) uuid = chrc.Get('org.bluez.GattCharacteristic1', 'UUID', dbus_interface='org.freedesktop.DBus.Properties') - self.characteristics[uuid] = chrc + characteristics[uuid] = chrc + for desc_path, desc_interfaces in iteritems(objects): + if "org.bluez.GattDescriptor1" not in desc_interfaces: + continue + if not desc_path.startswith(chrc.object_path): + continue + desc = bus.get_object("org.bluez", desc_path) + desc_uuid = desc.Get('org.bluez.GattDescriptor1', 'UUID', + dbus_interface='org.freedesktop.DBus.Properties') + if desc_uuid[4:8] != '2901': + continue + try: + readval = desc.ReadValue({}, dbus_interface='org.bluez.GattDescriptor1') + except dbus.exceptions.DBusException: + break + found_name = ''.join(chr(b) for b in readval).lower() + nu_lookup[found_name] = uuid + break + + match_found = True + for name in self.chrc_names: + if name not in nu_lookup: + # Endpoint name not present + match_found = False + break + + # Create lookup table only if all endpoint names found + self.nu_lookup = [None, nu_lookup][match_found] + self.characteristics = characteristics + service_found = True + + # If the service UUID matches that in the advertisement + # we can stop the search now. If it doesn't match, we + # have found the service corresponding to the fallback + # UUID, in which case don't break and keep searching + # for the advertised service + if srv_uuid == self.srv_uuid_adv: + break + + if not service_found: + self.device.Disconnect(dbus_interface='org.bluez.Device1') + if self.adapter: + self.adapter.RemoveDevice(self.device) + self.device = None + self.nu_lookup = None + self.characteristics = dict() + raise RuntimeError("Provisioning service not found") + + def get_nu_lookup(self): + return self.nu_lookup def has_characteristic(self, uuid): - if uuid in self.characteristics.keys(): + if uuid in self.characteristics: return True return False @@ -154,6 +221,8 @@ class BLE_Bluez_Client: if self.adapter: self.adapter.RemoveDevice(self.device) self.device = None + self.nu_lookup = None + self.characteristics = dict() if self.adapter_props: self.adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(0)) @@ -180,7 +249,7 @@ class BLE_Bluez_Client: # Console based BLE client for Cross Platform support class BLE_Console_Client: - def connect(self, devname, iface, srv_uuid): + def connect(self, devname, iface, chrc_names, fallback_srv_uuid): print("BLE client is running in console mode") print("\tThis could be due to your platform not being supported or dependencies not being met") print("\tPlease ensure all pre-requisites are met to run the full fledged client") @@ -189,11 +258,14 @@ class BLE_Console_Client: if resp != 'Y' and resp != 'y': return False print("BLECLI >> List available attributes of the connected device") - resp = input("BLECLI >> Is the service UUID '" + srv_uuid + "' listed among available attributes? [y/n] ") + resp = input("BLECLI >> Is the service UUID '" + fallback_srv_uuid + "' listed among available attributes? [y/n] ") if resp != 'Y' and resp != 'y': return False return True + def get_nu_lookup(self): + return None + def has_characteristic(self, uuid): resp = input("BLECLI >> Is the characteristic UUID '" + uuid + "' listed among available attributes? [y/n] ") if resp != 'Y' and resp != 'y': diff --git a/tools/esp_prov/transport/transport_ble.py b/tools/esp_prov/transport/transport_ble.py index d0d26a836..333d95105 100644 --- a/tools/esp_prov/transport/transport_ble.py +++ b/tools/esp_prov/transport/transport_ble.py @@ -27,19 +27,28 @@ class Transport_BLE(Transport): # Calculate characteristic UUID for each endpoint nu_lookup[name] = service_uuid[:4] + '{:02x}'.format( int(nu_lookup[name], 16) & int(service_uuid[4:8], 16)) + service_uuid[8:] - self.name_uuid_lookup = nu_lookup # Get BLE client module self.cli = ble_cli.get_client() # Use client to connect to BLE device and bind to service - if not self.cli.connect(devname=devname, iface='hci0', srv_uuid=service_uuid): + if not self.cli.connect(devname=devname, iface='hci0', + chrc_names=nu_lookup.keys(), + fallback_srv_uuid=service_uuid): raise RuntimeError("Failed to initialize transport") - # Check if expected characteristics are provided by the service - for name in self.name_uuid_lookup.keys(): - if not self.cli.has_characteristic(self.name_uuid_lookup[name]): - raise RuntimeError("'" + name + "' endpoint not found") + # Irrespective of provided parameters, let the client + # generate a lookup table by reading advertisement data + # and characteristic user descriptors + self.name_uuid_lookup = self.cli.get_nu_lookup() + + # If that doesn't work, use the lookup table provided as parameter + if self.name_uuid_lookup is None: + self.name_uuid_lookup = nu_lookup + # Check if expected characteristics are provided by the service + for name in self.name_uuid_lookup.keys(): + if not self.cli.has_characteristic(self.name_uuid_lookup[name]): + raise RuntimeError("'" + name + "' endpoint not found") def __del__(self): # Make sure device is disconnected before application gets closed From 13a3edee8a95518006d66c035cc90b2ffba5fbd7 Mon Sep 17 00:00:00 2001 From: Anurag Kar Date: Wed, 29 May 2019 13:42:40 +0530 Subject: [PATCH 3/3] protocomm_ble : Example updated to use custom 128bit service UUID Also removed old hardcoded UUIDs from README of esp_prov --- .../provisioning/ble_prov/main/app_prov.c | 31 ++++++++++++++++--- tools/esp_prov/README.md | 12 +++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/provisioning/ble_prov/main/app_prov.c b/examples/provisioning/ble_prov/main/app_prov.c index 646a8de2b..cc991aa15 100644 --- a/examples/provisioning/ble_prov/main/app_prov.c +++ b/examples/provisioning/ble_prov/main/app_prov.c @@ -59,9 +59,9 @@ static esp_err_t app_prov_start_service(void) /* Endpoint UUIDs */ protocomm_ble_name_uuid_t nu_lookup_table[] = { - {"prov-session", 0xFF51}, - {"prov-config", 0xFF52}, - {"proto-ver", 0xFF53}, + {"prov-session", 0x0001}, + {"prov-config", 0x0002}, + {"proto-ver", 0x0003}, }; /* Config for protocomm_ble_start() */ @@ -69,12 +69,33 @@ static esp_err_t app_prov_start_service(void) .service_uuid = { /* LSB <--------------------------------------- * ---------------------------------------> MSB */ - 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, - 0x00, 0x10, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, + 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, }, .nu_lookup_count = sizeof(nu_lookup_table)/sizeof(nu_lookup_table[0]), .nu_lookup = nu_lookup_table }; + + /* With the above selection of 128bit primary service UUID and + * 16bit endpoint UUIDs, the 128bit characteristic UUIDs will be + * formed by replacing the 12th and 13th bytes of the service UUID + * with the 16bit endpoint UUID, i.e. : + * service UUID : 021a9004-0382-4aea-bff4-6b3f1c5adfb4 + * masked base : 021a____-0382-4aea-bff4-6b3f1c5adfb4 + * ------------------------------------------------------ + * resulting characteristic UUIDs : + * 1) prov-session : 021a0001-0382-4aea-bff4-6b3f1c5adfb4 + * 2) prov-config : 021a0002-0382-4aea-bff4-6b3f1c5adfb4 + * 3) proto-ver : 021a0003-0382-4aea-bff4-6b3f1c5adfb4 + * + * Also, note that each endpoint (characteristic) will have + * an associated "Characteristic User Description" descriptor + * with 16bit UUID 0x2901, each containing the corresponding + * endpoint name. These descriptors can be used by a client + * side application to figure out which characteristic is + * mapped to which endpoint, without having to hardcode the + * UUIDs */ + uint8_t eth_mac[6]; esp_wifi_get_mac(WIFI_IF_STA, eth_mac); snprintf(config.device_name, sizeof(config.device_name), "%s%02X%02X%02X", diff --git a/tools/esp_prov/README.md b/tools/esp_prov/README.md index 9d7a0e919..8466e02ad 100644 --- a/tools/esp_prov/README.md +++ b/tools/esp_prov/README.md @@ -13,12 +13,12 @@ python esp_prov.py --transport < mode of provisioning : softap \ ble \ console > Usage of `esp-prov` assumes that the provisioning app has specific protocomm endpoints active. These endpoints are active in the provisioning examples and accept specific protobuf data structures: -| Endpoint Name | URI (HTTP server on ip:port) | UUID (BLE) | Description | -|---------------|------------------------------|--------------------------------------|-----------------------------------------------------------| -| prov-session | http://ip:port/prov-session | 0000ff51-0000-1000-8000-00805f9b34fb | Security endpoint used for session establishment | -| prov-config | http://ip:port/prov-config | 0000ff52-0000-1000-8000-00805f9b34fb | Endpoint used for configuring Wi-Fi credentials on device | -| proto-ver | http://ip:port/proto-ver | 0000ff53-0000-1000-8000-00805f9b34fb | Version endpoint for checking protocol compatibility | -| custom-config | http://ip:port/custom-config | NA | Optional endpoint for configuring custom credentials | +| Endpoint Name | URI (HTTP server on ip:port) | Description | +|---------------|------------------------------|-----------------------------------------------------------| +| prov-session | http://ip:port/prov-session | Security endpoint used for session establishment | +| prov-config | http://ip:port/prov-config | Endpoint used for configuring Wi-Fi credentials on device | +| proto-ver | http://ip:port/proto-ver | Version endpoint for checking protocol compatibility | +| custom-config | http://ip:port/custom-config | Optional endpoint for configuring custom credentials | # PARAMETERS