This is an old revision of the document!
This scenario presents how to create the Bluetooth Low Energy server device and corresponding client device. The server can be the sensor device which responds to the client with the results of the measurements. This can also be the output device, which we can control writing the data to. The client connects to a server and reads the data. This scenario presents the use of the concept of services and characteristics.
It is necessary to understand the principles of the Bluetooth Low Energy protocol with concepts of services, characteristics and descriptors. We will use in this scenario the knowledge of the advertising process so making the IoT_7 exercise is recommended.
This scenario is intended to be implemented using two BLE laboratory nodes. One of them is a server, while the second is a client. Here we will present the simplest implementation to be extended in further scenarios.
Task 1. Implement a program that operates as the BLE server which advertises itself and allows us to connect to. Task 2. Implement a client device, to read the exemplary data from a server.
You can use the beacon and simple client programs from IoT_7 as the starting point.
We will present the lab in a few steps. We begin with a simple program which advertises the device with a single service containing a single characteristic. It allows us to establish a connection with a client and, if successfully connected, responds with simple text data. In further steps, we will improve the program to make it more reliable, eg. by returning to the advertisement procedure after disconnecting.
Let's begin with a simple program which advertises a single service. The code should start with including Arduino and BLE libraries.
#include <Arduino.h> #include "BLEDevice.h" #include "BLEUtils.h" #include "BLEServer.h"
We need variables to hold the class pointers for advertising, the server instance, service, and characteristics.
BLEAdvertising *pAdvertising; //class for the advertising data BLEServer *pServer; //class for the BLE Server device BLEService *pService; //class for the BLE Service in the Server BLECharacteristic *pCharacteristic; //class for the characteristic
Any service or characteristic is identified with a unique UUID. If you use the standard service, like eg. Health Thermometer or Binary Sensor Service you can use a 16-bit UUID defined by Bluetooth SIG in the document [1]. The same document defines the UUIDs for characteristics.
While none of the standard UUIDs fits your device you need to define your own UUID. It must be 128-bit long and can be randomly generated with the tool available on the website https://www.uuidgenerator.net/[2]. The service UUID and characteristic UUID must differ. Although they can be completely different in many implementations the UUIDs of characteristics grouped under one service differ on one digit.
#define SERVICE_UUID "6e9b7b26-ca96-4774-b056-8ec5b759fd86" #define CHARACTERISTIC_UUID "6e9b7b27-ca96-4774-b056-8ec5b759fd86"
The setup() function creates and initialises the BLE device instance with the name “SUT BLE device”, and creates the server and the service within the server.
BLEDevice::init("SUT BLE device"); //initialise the BLE device pServer = BLEDevice::createServer(); pService = pServer->createService(SERVICE_UUID);
Data for reading or writing is organised using characteristics. In this example, we create one characteristic with reading and writing enabled, and the initial data as the “BLE onboard” text.
pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->setValue("BLE onboard");
In the advertising packet, we can add the service UUID.
pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true);
After preparing all the elements we can close the setup() function by starting the service and beginning advertising.
pService->start(); pAdvertising->start();
The loop function can remain empty or call the delay function periodically.
void loop(){ delay(1000); };
In this step, we will analyse the behaviour of the client. The client software is much more complex than the server. It is because the server, called also the central device, in many circumstances is a more powerful device than the peripheral. Some parts of the software are implemented as callback functions because they handle reactions on the data coming asynchronously from the server. The diagram presents the algorithm of the client and data coming from the server.
While we have analysed the client's behaviour we can start implementation. Let's begin with the libraries needed.
#include "Arduino.h" #include "BLEDevice.h"
Next, we define the UUIDs for remote service and a characteristic. Notice they must match the ones defined in the server.
// The remote service we wish to connect to. #define SERVICE_UUID "6e9b7b26-ca96-4774-b056-8ec5b759fd86" // The characteristic of the remote service we are interested in. #define REMOTE_CHARACTERISTIC_UUID "6e9b7b28-ca96-4774-b056-8ec5b759fd86"
Some global variables will be needed for our software.
// Variables static boolean doConnect = false; static boolean connected = false; static boolean doScan = false; static BLERemoteCharacteristic* pRemoteCharacteristic; static BLEAdvertisedDevice* myDevice;
Two callback functions will be defined. The first callback is for advertising. It is called whenever the advertising frame is received, no matter which device sends it. Inside the callback, we search for the device nearby which presents the service we would like to use. If we find one, we can create the instance of the remote device class and pass the signal to the main program to establish the connection.
// Scan for BLE servers and find the first one that advertises // the service we are looking for. class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { // We have found a device, let's see if it contains the service we are looking for. if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID))) { BLEDevice::getScan()->stop(); myDevice = new BLEAdvertisedDevice(advertisedDevice); doConnect = true; doScan = true; } // Found our server } // onResult }; // MyAdvertisedDeviceCallbacks
The second callback class helps to inform the other parts of the program about the connection close.
class MyClientCallback : public BLEClientCallbacks { void onConnect(BLEClient* pclient) { } void onDisconnect(BLEClient* pclient) { connected = false; } };
The setup function initialises the Bluetooth, registers the advertising callback function, and starts the scan to look for nearby devices.
void setup() { Serial.begin(115200); Serial.println("BLE Client app"); // Initialise the Bluetooth BLEDevice::init(""); // Retrieve the pointer to the scan module BLEScan* pBLEScan = BLEDevice::getScan(); // Register callback for incoming advertising pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); // Start scan parameters with active scan mode pBLEScan->setInterval(1349); pBLEScan->setWindow(449); pBLEScan->setActiveScan(true); // Start the scan for 30 seconds pBLEScan->start(30, false); }
In the loop function, we wait for the information that the server with the service UUID we were interested in was found. It is signalled with the use of “doConnect” flag. If it's true we can connect to this server. Once we are connected the “connected” flag is set to be true in the connectToServer() function. We will show this function in a while.
void loop() { if (doConnect == true) { connectToServer(); // Establish the connection to the server doConnect = false; } if (!connected) { if(doScan){ BLEDevice::getScan()->start(0); // Start scan again after disconnect } delay(1000); // Delay a second between loops. }
The connection to the server is executed in some steps:
bool connectToServer() { BLEClient* pClient = BLEDevice::createClient(); pClient->setClientCallbacks(new MyClientCallback()); // Connect to the remote BLE Server. pClient->connect(myDevice); // Obtain a reference to the service in the remote BLE server. BLERemoteService* pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID)); // Obtain a reference to the characteristic of the chosen service. pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(REMOTE_CHARACTERISTIC_UUID)); connected = true; return true; }
BLE peripheral device stops advertising after establishing a connection. While the connection is closed or lost it should restart advertising to be able to accept further connections. Doing it is possible with the use of callback functions, executed in selected events occurrence. We need to add the callback functions called on the connection and disconnection events. We will use two global boolean variables, one to store the state of the device, and another to control the single advertising start.
bool deviceConnected = false; bool advStarted = true; //first advertise starts automatically class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; }; void onDisconnect(BLEServer* pServer) { deviceConnected = false; } };
In the setup() function, just after the creation of the server we set up the callbacks.
pServer->setCallbacks(new MyServerCallbacks());
In the loop() function we have to program the starting of the advertising while the connection is ended. The startAdvertising() function should be called just once so the advStarted boolean variable helps us to control this process.
void loop(){ if (!deviceConnected && !advStarted) { pServer->startAdvertising(); // restart advertising advStarted = true; } if (deviceConnected){ advStarted = false; } delay(500); };
You should be able to find the device in the BLE neighbourhood. It should allow us to establish connection, read the characteristic data and write a new value. Notice that the modified value remains unchanged until the new start of the device.
What types of data can be sent with the characteristics?: The BLE characteristics can send any value as a single byte or a sequence of bytes. BLE does not interpret the data in characteristic in any way. Interpretation lies on the side of the application.
What if I need to read or write more data than a single text?: It is possible to implement many services in one device and add many characteristics to one service. Theoretically, the maximum number of characteristics in one service is 512, but the implementation and the memory available for the BLE library limit it.
What is the maximum length of the data in the characteristics?: The maximum length of the data in the characteristics in the BLE specification is 512 bytes.