ESP32 – BLE : Building an Echo Service GATT Server

As you would know I use ESP8266 in every possible way. It's probably the cheapest board with Wifi chip built-in. When I heard about ESP32, it was a dream come true. ESP32 packs both Wifi and BLE in one single module for less than $10. Given the size of developer fan following 8266 has, it's guaranteed that ESP32 will have a good support.

Two modes (Wifi/BLE) of connectivity gives many options for configuring an IoT device. In my future projects I am planning to use BLE for configuration and offline mobile sync. Wifi to connect to internet.

Image Credit: Espressif Systems. ESP32 is created and developed by Espressif Systems.

Image Credit: Espressif Systems. ESP32 is created and developed by Espressif Systems.

As a starting point I wrote my usual hello world and echo server. In this GATT server configuration. There is a single service with UUID c0de0001-feed-f00d-c0ff-eeb3d05ebeef with two characteristics. Client (Eg. Phone App) can connect to this server and then write to c0de0002. The server will respond by notifying on c0de0003. It either replies with "world" or just echoes whatever the user has sent. I have not done connection management, multi packet etc. That's probably for the next post. In this post we will learn to set up the services/characteristics, start the advertisement, send and receive data.

  • Install Arduino IDE
  • Install new ESP32-BLE package for Arduino IDE. Instructions are specific to your operating system. If you already have rduino-esp3 installed then remove it and reinstall.
  • Write or reuse the ESP32/Arduino Sketch
  • Run it and test it using NRFConnect App
  • Make changes and have fun
  • I use ESP32 Dev Board (nodemcu dev board which has built in programmer) and a Micro USB cable
/*
    Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleServer.cpp
    Ported to Arduino ESP32 by Evandro Copercini
    Extended by Thejesh GN
*/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>


#define SERVICE_UUID        "c0de0001-feed-f00d-c0ff-eeb3d05ebeef"
#define CHARACTERISTIC_UUID_TX "c0de0002-feed-f00d-c0ff-eeb3d05ebeef"
#define CHARACTERISTIC_UUID_RX "c0de0003-feed-f00d-c0ff-eeb3d05ebeef"

bool deviceConnected = false;
bool messageReceivedComplete = false;

BLECharacteristic *pCharacteristicRX;
BLECharacteristic *pCharacteristicTX;
int echoNumber = 0;
std::string message;

class EchoServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      //stop advt?
      Serial.println("Connected!");
    };

    void onDisconnect(BLEServer* pServer) {
      //restart advt?
      deviceConnected = false;
      Serial.println("Disconnected!");
    }
};



void respond(std::string  send_message){
  Serial.println("inside send_message");
  pCharacteristicTX->setValue(send_message);
  pCharacteristicTX->notify();
  Serial.println("sent send_message");

}



//This call back happens when the android client writes 
class ServerReadCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
        std::string rxValue = pCharacteristic->getValue();
        Serial.println("*********");
        Serial.print("Received Value: ");
        for (int i = 0; i < rxValue.length(); i++) {
          Serial.print(rxValue[i]);          
        }
        Serial.println();

        //add to message (as of now just one packet)        
        message = rxValue;

        //once you think all packets are received. As of now one packet
        messageReceivedComplete = true;

    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");

  BLEDevice::init("ThejEcho");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new EchoServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);
  
  pCharacteristicRX = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID_RX,                                         
                                         BLECharacteristic::PROPERTY_WRITE
                                       );

  pCharacteristicRX->addDescriptor(new BLE2902());

  pCharacteristicRX->setCallbacks(new ServerReadCallbacks());

  pCharacteristicTX = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID_TX,                                         
                                         BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
                                       );

  pCharacteristicTX->addDescriptor(new BLE2902());
  
  pService->start();
  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->start();
  Serial.println("Characteristic defined! Now you can read it in your phone!");
}

void loop() {
  // some delay before running repeatedly
  delay(1000);


  // if device is connected and all packets are received, then respond
  if(deviceConnected && messageReceivedComplete){
        messageReceivedComplete = false;        
       if(String("hello").c_str() == message){
          respond(String("world").c_str());
          Serial.println("sent world");
       }else{
         respond(message);
          Serial.println("echoed");
       }
  }
}

In my ideal world; I would respond as soon as I receive the data (i.e inside ServerReadCallbacks). But I couldn't call

pCharacteristicTX->notify();

outside the loop() function. If I did It would crash. I am still debugging that part. As of now I am responding from loop() and it works quite well. I am not sure what's the best way here. If you know a better way, please leave a comment.

I am still exploring the possibilities. More posts will follow in this series.

1 Response

  1. Tom says:

    Thanks for the great article! I’ve hit the same stumbling block of not being able to call pCharacteristicTX->notify(); from outside of the loop. Did you ever find a solution to this?