Generic Purpose NimBLE C++ Class for ESP-IDF

Ali Camdal
6 min readFeb 25, 2023

--

In this article we are going to develop a C++ class for NimBLE which is a BLE only component of ESP32. As it is known BLE is one of the most popular communication option nowadays. Since ESP32 is created for IoT applications that contain BLE and WiFi, NimBLE is one of the most important components of ESP-IDF. According to IDF documentation, for BLE-only usecases NimBLE is recommended due to its small code footprint and runtime memory usage. That is why we are going to develop a C++ class for NimBLE.

First of all sub components of this class is belong to H2Zero’s C++ repository for NimBLE. We will develop a class that is suitable for most use case scenarios. Before diving into code all files and codes can be found at my GitHub repo.

I am going to use PlatformIO which is an open-source extension of Visual Studio Code for embedded application development. Basically, this class contains easy to use methods for BLE projects.


class NimBLE : public BLEServerCallbacks, public BLECharacteristicCallbacks
{

private:
bool is_available = false;
bool deviceConnected = false;
friend void queueTask(void *pvParams);
void onConnect(BLEServer *pServer, BLEConnInfo &connInfo);
void onDisconnect(BLEServer *pServer, BLEConnInfo &connInfo, int reason);
void onWrite(BLECharacteristic *pCharacteristic, BLEConnInfo &connInfo);
const char SERVICE_UUID[37] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
const char CHARACTERISTIC_UUID_RX[37] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
const char CHARACTERISTIC_UUID_TX[37] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E";
char txBuf[20];
QueueHandle_t queue_handler;
BLEServer *pServer = NULL;
BLEService *pService;
BLECharacteristic *pTxCharacteristic;
BLECharacteristic *pRxCharacteristic;

public:
void initBle(string ble_name);
void startAdv(void);
bool isConnected(void);
void pushQueue(char *buf);
string rxValue = "";
string old_rxValue = "";
void (*bleEvent)(string msg, size_t select);
void setReaderHandler(void (*func)(string msg, size_t select));
string getData(void);
bool isDataAvailable(void);
};

Above we can see the header file of the NimBLE class. As we can see we have multiple public and private methods. Private methods, obviously, mostly used in-class operations and do not need to be accessed from outside. Public methods are available for public access. It can be seen from class definition it is inherited from two different classes. These are used for built-in methods of NimBLE which are onConnect, onDisconnect, onWrite. Since we are going to implement those functions into our custom class, we do not need to define these classes again in our code. Also we can use our private variables in these built-in callbacks without doing some crazy stuffs.

Main working principle of the class is exteremly simple. If there is any new data in the queue to send to client push it with queueTask. If there is any new data that is coming from client, handle it in onWrite function and send it to user reader function.

const char SERVICE_UUID[37] = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
const char CHARACTERISTIC_UUID_RX[37] = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
const char CHARACTERISTIC_UUID_TX[37] = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

We are going to use Nordic UART characteristics for this class, which can be seen above, but it can be changed from the header file.

bool is_available = false;
bool deviceConnected = false;
friend void queueTask(void *pvParams);
void onConnect(BLEServer *pServer, BLEConnInfo &connInfo);
void onDisconnect(BLEServer *pServer, BLEConnInfo &connInfo, int reason);
void onWrite(BLECharacteristic *pCharacteristic, BLEConnInfo &connInfo);

is_available and deviceConnected variables are private. But we can access them through two different public functions. Another special function is queueTask which is a FreeRTOS task that controls the flow of queue. If there is a new data that is pushed to queue, it will write it to Notify characteristic of NimBLE.

QueueHandle_t queue_handler;
BLEServer *pServer = NULL;
BLEService *pService;
BLECharacteristic *pTxCharacteristic;
BLECharacteristic *pRxCharacteristic;

Above variables are used for setting BLE up in the initial phase. pServer is used for creating a BLE server, while pService is used for creating service characteristics. Other two characteristics variables are used to initialize RX and TX characteristics of BLE.

public:
void initBle(string ble_name);
void startAdv(void);
bool isConnected(void);
void pushQueue(char *buf);
string rxValue = "";
string old_rxValue = "";
void (*bleEvent)(string msg, size_t select);
void setReaderHandler(void (*func)(string msg, size_t select));
string getData(void);
bool isDataAvailable(void);

First two public methods have to be invoked before using NimBLE. initBLE method is starting initializing with the name of the BLE server. startAdv is simply starts the advertisement of characteristics. isConnected, isDataAvailable and getData methods are returns connection status, available data status and the new data respectively. The only method that is beyond the beginner level is setReaderHandler method. This method takes an address of a function that will be used in handling new data. This address will be assigned to bleEvent pointer to be used in the class.

So header this is the end of the header file. Let’s look at the CPP file for diving into code.


void NimBLE::initBle(string ble_name)
{
BLEDevice::init(ble_name);
this->pServer = BLEDevice::createServer();
this->pServer->setCallbacks(this);
this->pService = this->pServer->createService(this->SERVICE_UUID);

this->pTxCharacteristic = this->pService->createCharacteristic(
this->CHARACTERISTIC_UUID_TX,
NIMBLE_PROPERTY::NOTIFY);

this->pRxCharacteristic = this->pService->createCharacteristic(
this->CHARACTERISTIC_UUID_RX,
NIMBLE_PROPERTY::WRITE);

this->pRxCharacteristic->setCallbacks(this);
this->queue_handler = xQueueCreate(200, sizeof(this->txBuf));
xTaskCreate(queueTask, "queue_task", 4096, this, 2, NULL);
}

void NimBLE::startAdv()
{
this->pService->start();
this->pServer->getAdvertising()->start();
}

Initializing and advertisement method of this class is very simple to understand. We created a server, set callbacks, create service, set RX and TX characteristics respectively. This flow is very common when we talk about Bluetooth settings. Only differences in initBLE method are setting a queue for creating data flow and starting a task for controlling this flow.


void NimBLE::onConnect(BLEServer *pServer, BLEConnInfo &connInfo)
{
this->deviceConnected = true;
}

void NimBLE::onDisconnect(BLEServer *pServer, BLEConnInfo &connInfo, int reason)
{
this->deviceConnected = false;
}

void NimBLE::onWrite(BLECharacteristic *pCharacteristic, BLEConnInfo &connInfo)
{
string income = pCharacteristic->getValue();
if (income.length() > 1)
{
this->rxValue = income;
this->is_available = true;
this->bleEvent(this->rxValue, 1);
}
}

Before any other methods we can look at the built-in callbacks. As it can be understood onConnect and onDisconnect methods are called when the connection status of BLE has been changed. These methods are changing the value of deviceConnected variable. Last callback is onWrite method. This method has been called if there is any data that is written in characteristics of BLE. When we get the data to income variable, we do control the length of it and then assign it to rxValue variable. Then we set the flag of is_available that show there is a new data in buffer. Lastly we send this data to user’s handler function.


bool NimBLE::isDataAvailable(void)
{
return this->is_available;
}

string NimBLE::getData(void)
{
this->is_available = false;
return this->rxValue;
}

void NimBLE::setReaderHandler(void (*func)(string msg, size_t select))
{
this->bleEvent = func;
}

As we can see above, we have isDataAvailable method for getting binary result of the query of if there is any new data. Also getData method can return the last data to us when we needed. setReaderHandler method can set a function for us to use it in our main code block.


void queueTask(void *pvParams)
{
NimBLE *_this = (NimBLE *)pvParams;
for (;;)
{
char rxBuf[20];
if (xQueueReceive(_this->queue_handler, &rxBuf, (TickType_t)5))
{
cout << "Sending : " << rxBuf << endl;
_this->pTxCharacteristic->setValue((uint8_t *)rxBuf, strlen(rxBuf));
_this->pTxCharacteristic->notify();
}
vTaskDelay(1);
}
}

bool NimBLE::isConnected()
{
return this->deviceConnected;
}

void NimBLE::pushQueue(char *buf)
{
xQueueSend(this->queue_handler, buf, (TickType_t)0);
}

Finally we have our last three methods which are isConnected, queueTask, and pushQueue. When we need to find out that the device is connected to any client or not we can use isConnected method. Also if we want to send a data to client we can give the data to pushQueue method. It will send it to client for us. queueTask is basically a FreeRTOS task that controls the queue flow. If there is a new data it will take it and set the characteristics.

This is the end of this article. For the next chapter we are going to control an ESP32 with NimBLE connection using a Graphical User Interface that is developed with Python3. This repo is open to use for any kind of project. Thank you for your time to read it.

Ciao.

--

--