This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| en:iot-open:practical:hardware:sut:esp32:iot_8 [2024/04/15 21:21] – [Result validation] ktokarz | en:iot-open:practical:hardware:sut:esp32:iot_8 [2025/04/12 11:37] (current) – [IoT8: BLE Communication with characteristics] ktokarz | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== IoT8: BLE Communication with characteristics ===== | ||
| + | This scenario presents how to create the Bluetooth Low Energy server device and the 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 by 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. | ||
| + | ===== Prerequisites ===== | ||
| + | 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 [[en: | ||
| + | |||
| + | ===== Suggested Readings and Knowledge Resources ===== | ||
| + | * [[en: | ||
| + | * [[en: | ||
| + | * [[en: | ||
| + | |||
| + | |||
| + | ===== Hands-on Lab Scenario ===== | ||
| + | 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 to be implemented ==== | ||
| + | **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. | ||
| + | |||
| + | ==== Start ==== | ||
| + | You can use the beacon and simple client programs from [[en: | ||
| + | |||
| + | ==== Steps ==== | ||
| + | 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. The program returns automatically to the advertisement procedure after disconnecting. | ||
| + | |||
| + | === Step 1 === | ||
| + | Let's begin with a simple program which advertises a single service. The code should start with including Arduino and BLE libraries. | ||
| + | <code c> | ||
| + | #include < | ||
| + | #include " | ||
| + | #include " | ||
| + | #include " | ||
| + | </ | ||
| + | We need variables to hold the class pointers for advertising, | ||
| + | < | ||
| + | BLEAdvertising | ||
| + | BLEServer | ||
| + | BLEService | ||
| + | BLECharacteristic | ||
| + | </ | ||
| + | We will also need two boolean variables to control the restarting of the advertising. In case of establishing the connection from the remote client, the server device stops advertising, | ||
| + | <code c> | ||
| + | bool deviceConnected = false; | ||
| + | bool advStarted = true; | ||
| + | </ | ||
| + | |||
| + | 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 ((https:// | ||
| + | < | ||
| + | Example of the standard UUIDs for service and characteristic: | ||
| + | Health Thermometer Service - 0x1809 | ||
| + | Temperature Measurement Characteristic - 0x2A1C | ||
| + | </ | ||
| + | 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:// | ||
| + | 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 | ||
| + | #define CHARACTERISTIC_UUID " | ||
| + | </ | ||
| + | |||
| + | Before we implement the setup() function, we have to create the callback function executed in case of connection and disconnection events. It allows us to control the advertising process with the deviceConnected flag. | ||
| + | |||
| + | <code c> | ||
| + | class MyServerCallbacks: | ||
| + | void onConnect(BLEServer* pServer) { | ||
| + | deviceConnected = true; | ||
| + | }; | ||
| + | |||
| + | void onDisconnect(BLEServer* pServer) { | ||
| + | deviceConnected = false; | ||
| + | } | ||
| + | }; | ||
| + | </ | ||
| + | |||
| + | The setup() function creates and initialises the BLE device instance with the name "SUT BLE device", | ||
| + | |||
| + | < | ||
| + | // Initialise the BLE device | ||
| + | BLEDevice:: | ||
| + | |||
| + | // Create BLE Server instance | ||
| + | pServer = BLEDevice:: | ||
| + | |||
| + | // Set callback function for handling server events | ||
| + | pServer-> | ||
| + | |||
| + | // Create the service in the server | ||
| + | pService = pServer-> | ||
| + | |||
| + | </ | ||
| + | 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" | ||
| + | < | ||
| + | // Create the characteristic in the service | ||
| + | pCharacteristic = pService-> | ||
| + | | ||
| + | | ||
| + | | ||
| + | ); | ||
| + | |||
| + | // Set the initial value of the characteristic. | ||
| + | // We will be able to read it by client | ||
| + | pCharacteristic-> | ||
| + | </ | ||
| + | In the advertising packet, we can add the service UUID. | ||
| + | < | ||
| + | // Get the pointer to the advertising object | ||
| + | pAdvertising = BLEDevice:: | ||
| + | |||
| + | // Add the service UUID to the advertising | ||
| + | pAdvertising-> | ||
| + | |||
| + | // Enable reading extended information with scan response packet | ||
| + | pAdvertising-> | ||
| + | </ | ||
| + | < | ||
| + | Due to the limited size of the advertisement packet, not all service UUIDs can be broadcast. Even more, service UUID does not have to appear in the advertising frame, but its presence makes it possible to scan nearby devices by their functionality, | ||
| + | </ | ||
| + | After preparing all the elements we can close the setup() function by starting the service and beginning advertising. | ||
| + | < | ||
| + | // Starting the service and advertising process | ||
| + | pService-> | ||
| + | pAdvertising-> | ||
| + | </ | ||
| + | In the loop function, we need to handle the restarting of advertising in case of disconnection of the client. | ||
| + | < | ||
| + | void loop(){ | ||
| + | if (!deviceConnected && !advStarted) { | ||
| + | pServer-> | ||
| + | advStarted = true; | ||
| + | } | ||
| + | |||
| + | if (deviceConnected){ | ||
| + | advStarted = false; | ||
| + | } | ||
| + | delay(500); | ||
| + | }; | ||
| + | </ | ||
| + | |||
| + | === Step 2 === | ||
| + | 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. | ||
| + | {{ en: | ||
| + | |||
| + | === Step 3 === | ||
| + | While we have analysed the client' | ||
| + | < | ||
| + | #include " | ||
| + | #include " | ||
| + | #include " | ||
| + | </ | ||
| + | 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 " | ||
| + | // The characteristic of the remote service we are interested in. | ||
| + | #define REMOTE_CHARACTERISTIC_UUID " | ||
| + | </ | ||
| + | |||
| + | 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; | ||
| + | </ | ||
| + | We need to add and configure the LCD to be able to observe the results. | ||
| + | <code c> | ||
| + | // LCD display pins and class declaration | ||
| + | #define LCD_RS 2 | ||
| + | #define LCD_ENABLE 1 | ||
| + | #define LCD_D4 39 | ||
| + | #define LCD_D5 40 | ||
| + | #define LCD_D6 41 | ||
| + | #define LCD_D7 42 | ||
| + | static Adafruit_LiquidCrystal lcd(LCD_RS, LCD_ENABLE, LCD_D4, LCD_D5, LCD_D6, LCD_D7); | ||
| + | </ | ||
| + | |||
| + | 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: | ||
| + | void onResult(BLEAdvertisedDevice advertisedDevice) { | ||
| + | // We will print the asterix for every device found | ||
| + | lcd.print(" | ||
| + | | ||
| + | // 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))) { | ||
| + | |||
| + | // Our device has the service we need. We can stop scanning. | ||
| + | BLEDevice:: | ||
| + | | ||
| + | // Create the instance of remote device | ||
| + | myDevice = new BLEAdvertisedDevice(advertisedDevice); | ||
| + | | ||
| + | // Pass information to other part of the program to connect to the device | ||
| + | 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. It will additionally inform us about the current state of the program. | ||
| + | < | ||
| + | class MyClientCallback : public BLEClientCallbacks { | ||
| + | void onConnect(BLEClient* pclient) { | ||
| + | lcd.setCursor(0, | ||
| + | lcd.print(" | ||
| + | } | ||
| + | |||
| + | void onDisconnect(BLEClient* pclient) { | ||
| + | lcd.setCursor(0, | ||
| + | lcd.print(" | ||
| + | lcd.setCursor(0, | ||
| + | connected = false; | ||
| + | } | ||
| + | };</ | ||
| + | |||
| + | The setup function initialises the Bluetooth, registers the advertising callback function, and starts the scan to look for nearby devices. | ||
| + | < | ||
| + | void setup() { | ||
| + | // Initialise the LCD and print the welcome message | ||
| + | lcd.begin(16, | ||
| + | delay(1000); | ||
| + | lcd.setCursor(0, | ||
| + | lcd.print(" | ||
| + | |||
| + | // Initialise the Bluetooth | ||
| + | BLEDevice:: | ||
| + | |||
| + | // Retrieve the pointer to the scan module | ||
| + | BLEScan* pBLEScan = BLEDevice:: | ||
| + | | ||
| + | // Register callback for incoming advertising | ||
| + | pBLEScan-> | ||
| + | | ||
| + | // Start scan parameters with active scan mode | ||
| + | pBLEScan-> | ||
| + | pBLEScan-> | ||
| + | pBLEScan-> | ||
| + | | ||
| + | // Print the message on the LCD | ||
| + | lcd.setCursor(0, | ||
| + | lcd.print(" | ||
| + | lcd.setCursor(0, | ||
| + | | ||
| + | // Start the scan for 30 seconds | ||
| + | pBLEScan-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 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 " | ||
| + | \\ | ||
| + | In a loop() function we will periodically read the characteristic value. It is a good practice to check if the characteristic is properly retrieved and readable before reading the value. | ||
| + | <code c> | ||
| + | void loop() { | ||
| + | if (doConnect == true) { | ||
| + | // Establish the connection to the server | ||
| + | connectToServer(); | ||
| + | doConnect = false; | ||
| + | } | ||
| + | |||
| + | if (!connected) { | ||
| + | if(doScan){ | ||
| + | // Start scan again after disconnect | ||
| + | BLEDevice:: | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | if (connected) { | ||
| + | lcd.setCursor(0, | ||
| + | | ||
| + | // Is the characteristic properly retrieved? | ||
| + | if(pRemoteCharacteristic != nullptr) | ||
| + | | ||
| + | // Is the characteristic readable? | ||
| + | if(pRemoteCharacteristic-> | ||
| + | | ||
| + | // We can safely read the value | ||
| + | lcd.print(pRemoteCharacteristic-> | ||
| + | } | ||
| + | delay(1000); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | The connection to the server is executed in some steps: | ||
| + | - Creation of the client object. | ||
| + | - Setting the callback class for handling disconnection. | ||
| + | - Connection to the remote device. | ||
| + | - Setting parameters of the connection. | ||
| + | - Getting the reference to the service. | ||
| + | - Getting the reference to the characteristic. | ||
| + | - Informing the main program with the " | ||
| + | |||
| + | < | ||
| + | bool connectToServer() { | ||
| + | BLEClient* | ||
| + | pClient-> | ||
| + | |||
| + | // Connect to the remote BLE Server. | ||
| + | pClient-> | ||
| + | | ||
| + | // Obtain a reference to the service in the remote BLE server. | ||
| + | BLERemoteService* pRemoteService = pClient-> | ||
| + | | ||
| + | // Obtain a reference to the characteristic of the chosen service. | ||
| + | pRemoteCharacteristic = pRemoteService-> | ||
| + | | ||
| + | connected = true; | ||
| + | return true; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | |||
| + | ==== Result validation ==== | ||
| + | You should be able to observe messages describing the process of searching for the server on the first line of LCD and during scanning asterisks appearing on the second line. After establishing the connection, the characteristic value should appear on the display' | ||
| + | ===== FAQ ===== | ||
| + | |||
| + | **What types of data can be sent with the characteristics? | ||
| + | \\ | ||
| + | **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, | ||
| + | \\ | ||
| + | **What is the maximum length of the data in the characteristics? | ||
| + | \\ | ||
| + | |||
| + | <WRAP noprint> | ||
| + | ===== Project information ===== | ||
| + | {{: | ||
| + | This Intellectual Output was implemented under the Erasmus+ KA2.\\ | ||
| + | Project IOT-OPEN.EU Reloaded – Education-based strengthening of the European universities, | ||
| + | Project number: 2022-1-PL01-KA220-HED-000085090. | ||
| + | |||
| + | **__Erasmus+ Disclaimer__**\\ | ||
| + | This project has been funded with support from the European Commission. \\ | ||
| + | This publication reflects the views of only the author, and the Commission cannot be held responsible for any use that may be made of the information contained therein. | ||
| + | |||
| + | **__Copyright Notice__**\\ | ||
| + | This content was created by the IOT-OPEN.EU Reloaded consortium, 2022, | ||
| + | The content is Copyrighted and distributed under CC BY-NC [[https:// | ||
| + | <figure label> | ||
| + | {{: | ||
| + | </ | ||
| + | </ | ||