ESP32 BLE server
I wanted to try out the BLE capabilities of the ESP32. I will not go into deep explanations how BLE works. Adafruit has a nice tutorial that explains details. To learn about BLE, check out the Introduction to Bluetooth Low Energy. As I have temperature and humidity sensors attached the logical step would be to set it up as an environment sensor. I checked the Bluetooth GATT services to find out if there is something matching defined. And there is already a matching service, the Environmental Sensing. There are many characteristics defined for this service, to keep it simple I chose to implement only the following:
Just for fun I added to the list
- Date Time – gives the current date and time and is used as well as notification for the client that updated data is available
- Digital Output – to simulate a virtual keypad on the BLE client and trigger functions in the ESP32
- Device Name – the name of the BLE server device
- Two String – to transmit the comfort ratio and the perception
Now using BLE on the ESP32 has been made quite easy by nkolban who put together the ESP32 BLE for Arduino library. The library is already included in the ESP32 Arduino framework, so there is no need to install it separate.
To use the library, we first need to include the header files
1 2 3 4 5 |
#include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <BLE2902.h> #include <BLEAdvertising.h> |
Then we define the UUIDs for the service and the characteristics
1 2 3 4 5 6 7 8 9 10 11 |
// List of Service and Characteristic UUIDs #define SERVICE_UUID 0x181A #define NOTIFICATION_UUID 0x2A08 #define TEMP_UUID 0x2A6E #define HUMID_UUID 0x2A6F #define HEAT_UUID 0x2A7A #define DEW_UUID 0x2A7B #define COMFORT_UUID "00002A3D-ead2-11e7-80c1-9a214cf093ae" // same as String characteristic 0x2A3D #define PERCEPTION_UUID "10002A3D-ead2-11e7-80c1-9a214cf093ae" // same as String characteristic 0x2A3D #define OUTPUT_UUID 0x2A57 #define DEVICENAME_UUID 0x2A00 |
Next is the pointers to the service class, the characteristic classes and the advertising class. In addition some flags and a global variable was defined for handling the BLE communications
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/** Characteristic for client notification */ BLECharacteristic *pCharacteristicNotify; /** Characteristic for temperature in celsius */ BLECharacteristic *pCharacteristicTemp; /** Characteristic for humidity in percent */ BLECharacteristic *pCharacteristicHumid; /** Characteristic for heat index in celsius */ BLECharacteristic *pCharacteristicHeatIndex; /** Characteristic for dew point in celsius */ BLECharacteristic *pCharacteristicDewPoint; /** Characteristic for environment comfort status */ BLECharacteristic *pCharacteristicComfort; /** Characteristic for environment perception status */ BLECharacteristic *pCharacteristicPerception; /** Characteristic for digital output */ BLECharacteristic *pCharacteristicOutput; /** Characteristic for device name */ BLECharacteristic *pCharacteristicDeviceName; /** BLE Advertiser */ BLEAdvertising* pAdvertising; /** BLE Service */ BLEService *pService; /** BLE Server */ BLEServer *pServer; /** Digital output value received from the client */ uint8_t digitalOut = 0; /** Flag for change in digital output value */ bool digOutChanged = false; |
The BLE library works with callbacks for BLE events. The first callback needed is for client connection and disconnection. Here we set as well a flag to see if any BLE client is connected.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * MyServerCallbacks * Callbacks for client connection and disconnection */ class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { bleConnected = true; }; void onDisconnect(BLEServer* pServer) { if (pServer->getConnectedCount() == 0) { bleConnected = false; } } }; |
Next callback is for client write requests. As we have defined a characteristic Digital Output, the client might write values to this characteristic. If this happens, this callback is called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * MyCallbackHandler * Callbacks for client write requests */ class MyCallbackHandler: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { // save the value for later use digitalOut = (uint8_t) value[0]; // set the flag that the value has changed digOutChanged = true; } } }; |
Now we can initialize the BLE server, create a server and attach the callbacks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * initBLEserver * Setup BLE server * Setup callbacks for server and pCharacteristicStatus * Start advertising the BLE service */ void initBLEserver() { // Initialize BLE BLEDevice::init(apName); // Create BLE Server pServer = BLEDevice::createServer(); // Set server callbacks pServer->setCallbacks(new MyServerCallbacks()); // Create BLE Service pService = pServer->createService(BLEUUID((uint16_t)SERVICE_UUID),20); |
Next we create the characteristic for Date time. This characteristic is different from the other ones, because it has an additional descriptor which required if the client should be notified if data has been updated.
1 2 3 4 5 6 7 8 9 |
// Create BLE Characteristic for Alert pCharacteristicNotify = pService->createCharacteristic( BLEUUID((uint16_t)NOTIFICATION_UUID), BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); // Create a BLE Descriptor for Alert pCharacteristicNotify->addDescriptor(new BLE2902()); |
The other characteristics are created similar, but the do not have PROPERTY_NOTIFY set and no descriptor attached. All these characteristics are read only. That is indicated by PROPERTY_READ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Create BLE Characteristic for Temperature pCharacteristicTemp = pService->createCharacteristic( BLEUUID((uint16_t)TEMP_UUID), BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Humidity pCharacteristicHumid = pService->createCharacteristic( BLEUUID((uint16_t)HUMID_UUID), BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Dew Point pCharacteristicDewPoint = pService->createCharacteristic( BLEUUID((uint16_t)DEW_UUID), BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Perception pCharacteristicPerception = pService->createCharacteristic( PERCEPTION_UUID, BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Comfort pCharacteristicComfort = pService->createCharacteristic( COMFORT_UUID, BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Heat Index pCharacteristicHeatIndex = pService->createCharacteristic( BLEUUID((uint16_t)HEAT_UUID), BLECharacteristic::PROPERTY_READ ); // Create BLE Characteristic for Device Name pCharacteristicDeviceName = pService->createCharacteristic( BLEUUID((uint16_t)DEVICENAME_UUID), BLECharacteristic::PROPERTY_READ ); pCharacteristicDeviceName->setValue((uint8_t*)apName,16); |
The characteristic for the Digital Output is different again, as the client can write to it. So we have to attach the above defined callback function to receive the value the client has written to it
1 2 3 4 5 6 |
// Create BLE Characteristic for Digital output pCharacteristicOutput = pService->createCharacteristic( BLEUUID((uint16_t)OUTPUT_UUID), BLECharacteristic::PROPERTY_WRITE ); pCharacteristicOutput->setCallbacks(new MyCallbackHandler()); |
And that’s it, now we can start the service and the advertising
1 2 3 4 5 6 7 8 |
// Start the service pService->start(); // Start advertising pAdvertising = pServer->getAdvertising(); pAdvertising->start(); } |
The complete code for the BLE initialization can be found in my Github Repo.
Now we have to fill the characteristic with the values from our sensors.
Most of it is done in the temperature module. First we check if a BLE client is attached. If there is no client attached, no actions are necessary
1 2 |
// Send notification if any BLE client is connected if ((pServer != NULL) && (pServer->getConnectedCount() != 0)) { |
If a client is connected we fill the characteristics with the measured/calculated values
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
uint8_t tempData[2]; uint16_t tempValue; tempValue = (uint16_t)(lastValues.temperature*100); tempData[1] = tempValue>>8; tempData[0] = tempValue; pCharacteristicTemp->setValue(tempData, 2); tempValue = (uint16_t)(lastValues.humidity*100); tempData[1] = tempValue>>8; tempData[0] = tempValue; pCharacteristicHumid->setValue(tempData, 2); tempValue = (uint16_t)(dewPoint); tempData[1] = 0; tempData[0] = tempValue; pCharacteristicDewPoint->setValue(tempData, 2); tempValue = (uint16_t)(heatIndex); tempData[1] = 0; tempData[0] = tempValue; pCharacteristicHeatIndex->setValue(tempData, 2); String bleStatus = "Comfort: " + comfortStatus; size_t dataLen = bleStatus.length(); pCharacteristicComfort->setValue((uint8_t*)&bleStatus[0], dataLen); bleStatus = "Perception: " + humanPerception; dataLen = bleStatus.length(); pCharacteristicPerception->setValue((uint8_t*)&bleStatus[0], dataLen); |
Next is to setup the Date Time characteristic, which is used to notify the client about updated data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Send notification to connected clients uint8_t notifData[8]; time_t now; struct tm timeinfo; time(&now); // get time (as epoch) localtime_r(&now, &timeinfo); // update tm struct with current time uint16_t year = timeinfo.tm_year+1900; notifData[1] = year>>8; notifData[0] = year; notifData[2] = timeinfo.tm_mon+1; notifData[3] = timeinfo.tm_mday; notifData[4] = timeinfo.tm_hour; notifData[5] = timeinfo.tm_min; notifData[6] = timeinfo.tm_sec; pCharacteristicNotify->setValue(notifData, 8); |
And then we send the notification to the client
1 2 |
pCharacteristicNotify->notify(); } |
For changes in the Digital Output we just check the status of digOutChanged. This flag is set if the callbacks for client write requests was called. For a simple example, I just check the lowest three bits and start different tasks, depending on whether the bit was set or not.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Check if new digital output value was received on BLE if (digOutChanged) { digOutChanged = false; // reset the flag if (((digitalOut & 0x01) == 0x01) && (lightTaskHandle != NULL)) { xTaskResumeFromISR(lightTaskHandle); // start light measurement } if (((digitalOut & 0x02) == 0x02) && (tempTaskHandle != NULL)) { xTaskResumeFromISR(tempTaskHandle); // start temperature measurement } if (((digitalOut & 0x04) == 0x04) && (weatherTaskHandle != NULL)) { xTaskResumeFromISR(weatherTaskHandle); // Update weather from WU URL } } |







21:28
Is the complete .INO available?
Seems to be a lot of the program missing.
Thanks!
21:38
There is no INO file because I am using PlatformIO, not ArduinoIDE.
And the code went through several changes since I wrote this post.
The code I used at that time is here: https://github.com/beegee-tokyo/ESP32-Weatherstation/blob/7faf80a3f79639164d5e4414b81a98ecc128e06b/src/ble_server.cpp