Minimal ESP32 NimBLE example of a custom BLE service
In our previous post ESP32 minimal BLE Device Information Service (DIS) example we showed how to use the predefined DIS
(Device Information Service) with the espressif/ble_services
package using NimBLE as a backend.
Based on this, here’s how to create a completely custom BLE service:
CustomBLE.hpp
#pragma once
#include <string>
/**
* @brief Initialize custom NimBLE service with a string characteristic
*/
void InitCustomBLE();
/**
* @brief Update the string value of the custom characteristic
* @param value The new string value to set
*/
void UpdateCustomString(const std::string& value);
/**
* @brief Get the current string value of the custom characteristic
* @return Current string value
*/
std::string GetCustomString();
/**
* @brief Set GAP event handler for connection management
* Call this before starting NimBLE host
*/
void SetCustomBLEGapHandler();
CustomBLE.cpp
#include "CustomBLE.hpp"
#include "esp_log.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_uuid.h"
#include "host/ble_gatt.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include <string>
#include <cstring>
static const char *TAG = "CustomBLE";
// Custom service and characteristic UUIDs (randomly generated 128-bit UUIDs)
static const ble_uuid128_t custom_service_uuid =
BLE_UUID128_INIT(0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12,
0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12);
static const ble_uuid128_t custom_char_uuid =
BLE_UUID128_INIT(0x98, 0xBA, 0xDC, 0xFE, 0x21, 0x43, 0x65, 0x87,
0x98, 0xBA, 0xDC, 0xFE, 0x21, 0x43, 0x65, 0x87);
static std::string custom_string_value = "Hello, NimBLE!";
static uint16_t custom_char_handle;
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
// GAP event listener structure
static struct ble_gap_event_listener gap_event_listener;
// GATT characteristic access function
static int custom_char_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
int rc;
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
ESP_LOGI(TAG, "Custom characteristic read");
rc = os_mbuf_append(ctxt->om, custom_string_value.c_str(), custom_string_value.length());
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
ESP_LOGI(TAG, "Custom characteristic write");
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
if (om_len > 0) {
char buffer[om_len + 1];
rc = ble_hs_mbuf_to_flat(ctxt->om, buffer, sizeof(buffer) - 1, NULL);
if (rc == 0) {
buffer[om_len] = '\0';
custom_string_value = std::string(buffer);
ESP_LOGI(TAG, "Custom string updated to: %s", custom_string_value.c_str());
}
}
return 0;
}
default:
return BLE_ATT_ERR_UNLIKELY;
}
}
// GATT service definition
static const struct ble_gatt_svc_def custom_gatt_svcs[] = {
{
BLE_GATT_SVC_TYPE_PRIMARY,
&custom_service_uuid.u,
NULL, // includes
(struct ble_gatt_chr_def[]) {
{
&custom_char_uuid.u,
custom_char_access,
NULL, // arg
NULL, // descriptors
BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
0, // min_key_size
&custom_char_handle,
NULL, // cpfd
},
{
NULL, // uuid - end of characteristics
}
},
},
{
0, // type - end of services
}
};
// GAP event handler
static int custom_gap_event(struct ble_gap_event *event, void *arg) {
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI(TAG, "Connection %s; status=%d",
event->connect.status == 0 ? "established" : "failed",
event->connect.status);
if (event->connect.status == 0) {
conn_handle = event->connect.conn_handle;
}
break;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "Disconnect; reason=%d", event->disconnect.reason);
conn_handle = BLE_HS_CONN_HANDLE_NONE;
break;
default:
break;
}
return 0;
}
void InitCustomBLE() {
int rc;
// Initialize GATT services
rc = ble_gatts_count_cfg(custom_gatt_svcs);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to count GATT services: %d", rc);
return;
}
rc = ble_gatts_add_svcs(custom_gatt_svcs);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to add GATT services: %d", rc);
return;
}
ESP_LOGI(TAG, "Custom NimBLE service initialized");
}
void UpdateCustomString(const std::string& value) {
custom_string_value = value;
ESP_LOGI(TAG, "Custom string value updated to: %s", custom_string_value.c_str());
// If connected, send notification
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
struct os_mbuf *om;
om = ble_hs_mbuf_from_flat(value.c_str(), value.length());
if (om != NULL) {
int rc = ble_gattc_notify_custom(conn_handle, custom_char_handle, om);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to send notification: %d", rc);
}
}
}
}
std::string GetCustomString() {
return custom_string_value;
}
void SetCustomBLEGapHandler() {
// Set up the GAP event listener
gap_event_listener.fn = custom_gap_event;
gap_event_listener.arg = NULL;
ble_gap_event_listener_register(&gap_event_listener, custom_gap_event, NULL);
}
How to integrate with InitBLE()
This integrates well with BLE.cpp
from our previous example ESP32 minimal BLE Device Information Service (DIS) example. Here’s the InitBLE()
function that initializes both the custom service and the Device Information Service.
The only thing you need to add here is to
SetCustomBLEGapHandler();
InitCustomBLE(); // Initialize custom BLE service
after esp_nimble_init()
and before starting the BLE host via esp_ble_conn_start()
.
Full InitBLE()
function:
#include "CustomBLE.hpp"
/* ... */
void InitBLE(void)
{
esp_nimble_init();
GenerateDeviceName();
esp_ble_conn_config_t config;
strncpy((char*)config.device_name, device_name.c_str(), sizeof(config.device_name) - 1);
strncpy((char*)config.broadcast_data, "Metexon", sizeof(config.broadcast_data) - 1);
esp_err_t ret;
// Initialize NVS
esp_event_handler_register(BLE_CONN_MGR_EVENTS, ESP_EVENT_ANY_ID, app_ble_conn_event_handler, NULL);
esp_ble_conn_init(&config);
/**
* Initialize device information service (DIS)
*/
app_ble_dis_init();
SetCustomBLEGapHandler();
InitCustomBLE(); // Initialize custom BLE service
/**
* Start BLE (in separate thread)
*/
if (esp_ble_conn_start() != ESP_OK) {
esp_ble_conn_stop();
esp_ble_conn_deinit();
esp_event_handler_unregister(BLE_CONN_MGR_EVENTS, ESP_EVENT_ANY_ID, app_ble_conn_event_handler);
}
}
How to test
Use our script from Minimal Python script to list & read BLE device characteristics using Python (Bleak):
Partial example output
Service: 12345678-9abc-def0-1234-56789abcdef0
Description: Unknown
Handle: 1
Characteristics (1):
----------------------------------------------------------------------------
UUID: 87654321-fedc-ba98-8765-4321fedcba98
Description: Unknown
Handle: 2
Properties: read, write, notify
Value (string): Hello, NimBLE!