I'm working on this project where I'm using two ESP32-C3 microcontrollers. I'm trying to use BLE ( NimBLE stack specifically ) to connect them to share some data. I never worked with BLE or anything similar before so this was quite challenging. I got the two ESP32s to connect but I still can't share any data because I get some errors when doing service discovery on the client-side. Note: I downloaded some phone app called nRF Connect that enables me to connect to the server ESP32 and share data. I can successfully share data using that phone app, meaning, my server side ESP32' code is kind of "OK". The problem should be in the client-side code, regarding those service discovery errors.
I'll provide the code for both server and client side, but don't judge me, it's not really clean and I had ChatGPT help me a lot understanding these things, so the issue might even be my misunderstanding of something. Also, I took a lot of inspiration from other people' code examples, specifically SIMS-IOT guy from YouTube.
SERVER-SIDE
// NimBLE Server
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_uuid.h"
#include "sdkconfig.h"
#define MY_SERVICE_UUID BLE_UUID16_DECLARE(0x1405)
#define MY_READ_CHR_UUID BLE_UUID16_DECLARE(0x0508)
#define MY_WRITE_CHR_UUID BLE_UUID16_DECLARE(0x0512)
char *TAG = "BLE-Server";
uint8_t ble_addr_type;
void ble_app_advertise(void);
static uint16_t timestamp_value = 69;
static int timestamp_read(uint16_t con_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
os_mbuf_append(ctxt->om, ×tamp_value, sizeof(uint16_t));
return 0;
}
static int timestamp_write(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
printf("Data from the client: %.*s\n", ctxt->om->om_len, ctxt->om->om_data);
if (ctxt->om->om_len == sizeof(uint16_t))
{
memcpy(×tamp_value, ctxt->om->om_data, sizeof(uint16_t));
printf("%u\n", timestamp_value);
}
return 0;
}
static const struct ble_gatt_svc_def gatt_svcs[] = {
{.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = MY_SERVICE_UUID, // UUID for service
.characteristics = (struct ble_gatt_chr_def[]){
{.uuid = MY_READ_CHR_UUID, // UUID for reading
.flags = BLE_GATT_CHR_F_READ,
.access_cb = timestamp_read},
{.uuid = MY_WRITE_CHR_UUID, // UUID for writing
.flags = BLE_GATT_CHR_F_WRITE,
.access_cb = timestamp_write},
{0}}},
{0}};
// BLE event handling
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
switch (event->type)
{
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s",
event->connect.status == 0 ? "OK!" : "FAILED!");
if (event->connect.status == 0) {
printf("Connection established with client\n");
} else {
printf("Connection failed with status: %d\n", event->connect.status);
ble_app_advertise();
}
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI("GAP", "BLE GAP EVENT ADVERTISEMENT COMPLETE");
ble_app_advertise();
break;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI("GAP", "BLE DEVICE DISCONNECTED");
ble_app_advertise();
break;
default:
break;
}
return 0;
}
// Define the BLE connection
void ble_app_advertise(void)
{
// GAP - device name definition
struct ble_hs_adv_fields fields;
const char *device_name;
memset(&fields, 0, sizeof(fields));
device_name = ble_svc_gap_device_name(); // Read the BLE device name
fields.name = (uint8_t *)device_name;
fields.name_len = strlen(device_name);
fields.name_is_complete = 1;
// Include the list of service UUIDs in the advertisement
fields.uuids16 = MY_SERVICE_UUID;
fields.num_uuids16 = 1;
ble_gap_adv_set_fields(&fields);
// GAP - device connectivity definition
struct ble_gap_adv_params adv_params;
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;
ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);
}
// The application
void ble_app_on_sync(void)
{
ble_hs_id_infer_auto(0, &ble_addr_type);
ble_app_advertise(); // Define the BLE connection
}
// The infinite task
void host_task(void *param)
{
nimble_port_run();
}
void app_main()
{
nvs_flash_init(); // 1 - Initialize NVS flash using
esp_nimble_hci_init(); // 2 - Initialize ESP controller
nimble_port_init(); // 3 - Initialize the host stack
ble_svc_gap_device_name_set("BLE-Server"); // 4 - Initialize NimBLE configuration - server name
ble_svc_gap_init(); // 4 - Initialize NimBLE configuration - gap service
ble_svc_gatt_init(); // 4 - Initialize NimBLE configuration - gatt service
ble_gatts_count_cfg(gatt_svcs); // 4 - Initialize NimBLE configuration - config gatt services
ble_gatts_add_svcs(gatt_svcs); // 4 - Initialize NimBLE configuration - queues gatt services.
ble_hs_cfg.sync_cb = ble_app_on_sync; // 5 - Initialize application
nimble_port_freertos_init(host_task); // 6 - Run the thread
}
CLIENT-SIDE
// NimBLE Client
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "host/ble_gatt.h"
#include "sdkconfig.h"
#define MY_SERVICE_UUID BLE_UUID16_DECLARE(0x1405)
#define MY_READ_CHR_UUID BLE_UUID16_DECLARE(0x0508)
#define MY_WRITE_CHR_UUID BLE_UUID16_DECLARE(0x0512)
uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
uint16_t attr_handle = 0;
uint16_t read_handle = 0;
uint16_t write_handle = 0;
bool writable_chr_discovered = false;
static bool service_discovery_in_progress = false;
struct ble_hs_adv_fields fields;
char *TAG = "BLE Client Scan";
uint8_t ble_addr_type;
void ble_app_scan(void);
static uint16_t timestamp_value = 0;
static int on_read(uint16_t conn_handle, int status, struct ble_gatt_attr *attr, void *arg)
{
if (status == 0 && attr != NULL)
{
if (attr->om->om_len == sizeof(uint16_t))
{
memcpy(×tamp_value, attr->om->om_data, sizeof(uint16_t));
printf("Received timestamp value from the server: %u\n", timestamp_value);
}
}
else
{
printf("Read operation failed with status: %d\n", status);
}
os_mbuf_free_chain(attr->om);
return 0;
}
// Characteristic discovery callback
static int on_disc_chr(uint16_t conn_handle, const struct ble_gatt_error *error,
const struct ble_gatt_chr *chr, void *arg)
{
int rc = 0;
ESP_LOGI("GAP", "1\n");
if (error == NULL && chr != NULL)
{
if (ble_uuid_cmp(&chr->uuid, MY_READ_CHR_UUID) == 0)
{
read_handle = chr->val_handle;
} else if (ble_uuid_cmp(&chr->uuid, MY_WRITE_CHR_UUID) == 0)
{
write_handle = chr->val_handle;
printf("Found writable characteristic. Handle: %d\n", write_handle);
writable_chr_discovered = true;
ESP_LOGI("GAP", "2\n");
}
}
return 0;
}
// Service discovery callback
static int on_disc_svc(uint16_t conn_handle, const struct ble_gatt_error *error,
const struct ble_gatt_svc *service, void *arg)
{
ESP_LOGI("GAP", "3\n");
if (error == NULL)
{
if (service != NULL)
{
int rc = ble_gattc_disc_all_chrs(conn_handle, service->start_handle, service->end_handle, on_disc_chr, NULL);
if (rc != 0)
{
printf("Failed to discover characteristics for service (error: %d)\n", rc);
ESP_LOGI("GAP", "4\n");
}
else
{
printf("Discovered a service!\n");
ESP_LOGI("GAP", "5\n");
ble_gap_disc_cancel();
}
}
}
else
{
printf("Service discovery error: %d\n", error->status);
}
service_discovery_in_progress = false;
return 0;
}
void ble_app_write_timestamp(uint16_t conn_handle, uint16_t write_handle, uint16_t timestamp_value)
{
struct os_mbuf *om;
om = ble_hs_mbuf_from_flat(×tamp_value, sizeof(timestamp_value));
ESP_LOGI("GAP", "6\n");
if (om != NULL)
{
int rc = ble_gattc_write_flat(conn_handle, write_handle, om->om_data, om->om_len, NULL, NULL);
if (rc != 0)
{
printf("Error writing timestamp value: %d\n", rc);
ESP_LOGI("GAP", "7\n");
}
os_mbuf_free_chain(om);
}
}
// BLE connection event handling callback
static int ble_gap_connect_cb(struct ble_gap_event *event, void *arg)
{
int rc = 0;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI("GAP", "GAP EVENT CONNECTED");
conn_handle = event->connect.conn_handle;
printf("Connected with %.*s\n", fields.name_len, fields.name);
vTaskDelay(pdMS_TO_TICKS(1000));
if (!service_discovery_in_progress) {
service_discovery_in_progress = true;
rc = ble_gattc_disc_all_svcs(event->connect.conn_handle, on_disc_svc, NULL);
if (rc != 0) {
printf("Failed to initiate service discovery (error: %d)\n", rc);
service_discovery_in_progress = false;
}
} else {
printf("Service discovery already in progress, skipping.\n");
}
break;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI("GAP", "GAP EVENT DISCONNECTED");
conn_handle = BLE_HS_CONN_HANDLE_NONE;
writable_chr_discovered = false;
service_discovery_in_progress = false;
break;
default:
break;
}
return 0;
}
// BLE event handling
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
struct ble_gap_conn_params conn_params;
memset(&conn_params, 0, sizeof(conn_params));
int rc = 0;
switch (event->type)
{
case BLE_GAP_EVENT_DISC:
ESP_LOGI("GAP", "GAP EVENT DISCOVERY");
ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);
if(fields.name_len > 0)
{
printf("Discovered %.*s\n", fields.name_len, fields.name);
}
ble_gap_disc_cancel();
conn_params.scan_itvl = BLE_GAP_SCAN_FAST_INTERVAL_MAX;
conn_params.scan_window = BLE_GAP_SCAN_FAST_WINDOW;
conn_params.itvl_min = BLE_GAP_INITIAL_CONN_ITVL_MIN;
conn_params.itvl_max = BLE_GAP_INITIAL_CONN_ITVL_MAX;
conn_params.latency = BLE_GAP_INITIAL_CONN_LATENCY;
conn_params.supervision_timeout = BLE_GAP_INITIAL_SUPERVISION_TIMEOUT;
conn_params.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN;
conn_params.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN;
rc = ble_gap_connect(BLE_OWN_ADDR_PUBLIC, &event->disc.addr, 1000, &conn_params, ble_gap_connect_cb, NULL);
vTaskDelay(pdMS_TO_TICKS(400));
if (rc != 0)
{
printf("Failed to initiate connection (error: %d)\n", rc);
}
break;
default:
break;
}
return 0;
}
void ble_app_scan(void)
{
struct ble_gap_disc_params disc_params;
printf("Start scanning ...\n");
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
disc_params.itvl = 128;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
ble_gap_disc(ble_addr_type, BLE_HS_FOREVER, &disc_params, ble_gap_event, NULL);
}
// The application
void ble_app_on_sync(void)
{
ble_hs_id_infer_auto(0, &ble_addr_type);
vTaskDelay(pdMS_TO_TICKS(1000));
ble_app_scan();
}
// The infinite task
void host_task(void *param)
{
nimble_port_run();
}
void app_main()
{
nvs_flash_init(); // 1 - Initialize NVS flash using
esp_nimble_hci_init(); // 2 - Initialize ESP controller
nimble_port_init(); // 3 - Initialize the controller stack
esp_log_level_set("*", ESP_LOG_INFO); // Initialize Logging System with log level INFO
ble_svc_gap_device_name_set("BLE-Client"); // 4 - Set device name characteristic
ble_hs_cfg.sync_cb = ble_app_on_sync; // 4 - Set application
ble_svc_gap_init(); // 5 - Initialize GAP service
nimble_port_freertos_init(host_task); // 6 - Set infinite task
}
So, I'm trying to send a simple timestamp value from the client to the server, and I was planning on doing the same thing in reverse. For testing purposes, I used a simple uint16_t integer as the "timestamp" value. I wanted to send that value using the ble_app_write_timestamp() function I implemented. I didn't call it in the provided code because I was trying to figure out some errors that occurred while doing service discovery. I put some log statements in my code to help me figure out where are things going wrong, and this is the output I get from the client:
Start scanning ...
I (1367) NimBLE: GAP procedure initiated: discovery;
I (1367) NimBLE: own_addr_type=0 filter_policy=0 passive=0 limited=0 filter_duplicates=1
I (1367) NimBLE: duration=forever
I (1367) NimBLE:
I (1407) GAP: GAP EVENT DISCOVERY
Discovered BLE-Server
I (1407) NimBLE: GAP procedure initiated: connect;
I (1407) NimBLE: peer_addr_type=0 peer_addr=
I (1407) NimBLE: 58:cf:79:0c:84:b2
I (1417) NimBLE: scan_itvl=96 scan_window=48 itvl_min=24 itvl_max=40 latency=0 supervision_timeout=256 min_ce_len=0 max_ce_len=0 own_addr_type=0
I (1427) NimBLE:
I (1837) GAP: GAP EVENT CONNECTED
Connected with BLE-Server
I (2837) NimBLE: GATT procedure initiated: discover all services
I (2917) GAP: 3
Service discovery error: 0
I (2917) GAP: 3
Service discovery error: 0
I (2917) GAP: 3
Service discovery error: 0
I (2927) GAP: 3
Service discovery error: 14
And this is the output from the server:
I (367) NimBLE: GAP procedure initiated: stop advertising.
I (367) NimBLE: GAP procedure initiated: advertise;
I (377) NimBLE: disc_mode=2
I (377) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0
I (387) NimBLE:
I (6117) GAP: BLE GAP EVENT CONNECT OK!
Connection established with client
I didn't copy all of the output because there's a lot of initialization stuff that's hopefully not relevant. Anyway, my question is, if you can help me figure out why can't I discover the server services on client-side?
BTW, I'm using ESP-IDF enviroment in VS Code, and I'm programming this in C of course.