This is an old revision of the document!


Lab 2. Bluetooth Low Energy (BLE)

In this laboratory you will learn how to build a Bluetooth Low Energy (BLE) environmental service on your ESP32 Sparrow boards and to expose the measured sensor data to a Web application.

Bluetooth Smart (Bluetooth Low Energy)

Bluetooth Smart, also known as Bluetooth Low Energy, abbreviated as BLE, is an energy-efficient iteration of Bluetooth designed to conserve power. Its main use involves transmitting small amounts of data over short distances with low bandwidth.

Unlike the constant activity of regular Bluetooth, BLE typically stays in sleep mode, only activating when a connection is established.

This results in significantly lower power consumption, approximately 100 times less than traditional Bluetooth, depending on the specific application. To explore the key distinctions between Bluetooth and Bluetooth Low Energy, refer to the detailed comparison here.

Network Architecture

Within Bluetooth Low Energy, there exist two device types: the server (referred to as peripheral) and the client. The ESP32 is versatile, capable of functioning as either a client or a server.

The server actively broadcasts its presence, making it discoverable by other devices, and it holds data that the client can access. Meanwhile, the client conducts scans of nearby devices. Upon locating the desired server, it initiates a connection and awaits incoming data. This mode of communication is termed point-to-point, and it is the communication mode we will employ with the ESP32 Sparrow.

GATT

GATT, which stands for Generic Attributes, establishes a hierarchical data structure accessible to connected BLE devices. In essence, GATT outlines the protocol governing the exchange of standard messages between two BLE devices. Grasping this hierarchical arrangement is crucial as it facilitates a clearer comprehension of how to effectively employ BLE with the ESP32.

  • Profile: standard collection of services for a specific use case;
  • Service: collection of related information, like sensor readings, battery level, heart rate, etc. ;
  • Characteristic: it is where the actual data is saved on the hierarchy (value);
  • Descriptor: metadata about the data;
  • Properties: describes how the characteristic value can be interacted with. For example: read, write, notify, broadcast, indicate, etc.

BLE Profiles

At the highest level of the hierarchy is a profile, consisting of one or more services. Typically, a BLE device encompasses multiple services.

Each service comprises at least one characteristic, and it may also reference other services. Essentially, a service serves as a repository of information, such as sensor readings.

The Bluetooth Special Interest Group (SIG) has established predefined services for various data types, including Battery Level, Blood Pressure, Heart Rate, Weight Scale, etc. Additional defined services can be explored here.

BLE Characteristic

A characteristic is invariably associated with a service, serving as the location within the hierarchy where the actual data resides (value). It consistently consists of two attributes: the characteristic declaration, offering metadata about the data, and the characteristic value.

Furthermore, the characteristic value may be accompanied by descriptors, providing additional details about the metadata specified in the characteristic declaration.

The properties delineate the ways in which interaction with the characteristic value can occur. Essentially, these properties encompass the operations and procedures applicable to the characteristic:

  • Broadcast
  • Read
  • Write without response
  • Write
  • Notify
  • Indicate
  • Authenticated Signed Writes
  • Extended Properties

UUID

Every service, characteristic, and descriptor possesses a Universally Unique Identifier (UUID), which is a distinct 128-bit (16 bytes) number, for instance: 55072829-bc9e-4c53-938a-74a6d4c78776

Shortened UUIDs are available for all types, services, and profiles outlined in the Bluetooth Special Interest Group (SIG) specifications.

Should your application require a custom UUID, you can generate one using a UUID generator website, but for this lab assignment we will use the default UUID for an Environmental Sensing Service.

In essence, the UUID serves the purpose of uniquely identifying information. For example, it can distinguish a specific service provided by a Bluetooth device.

Build the example below, which advertises the board on BLE. Install on your phone an app that scans nearby Bluetooth devices, such as nRF Connect. Check if your device is in the list.

main.cpp
#include <Arduino.h>
#include <NimBLEDevice.h>
 
static const char* DEVICE_NAME = "ESP32-C6 Demo";
static NimBLEUUID SERVICE_UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
static NimBLEUUID CHAR_UUID   ("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
 
NimBLEServer*         gServer = nullptr;
NimBLEService*        gService = nullptr;
NimBLECharacteristic* gChar = nullptr;
 
void startBLE() {
  NimBLEDevice::init(DEVICE_NAME);
 
  gServer  = NimBLEDevice::createServer();
  gService = gServer->createService(SERVICE_UUID);
 
  gChar = gService->createCharacteristic(
    CHAR_UUID,
    NIMBLE_PROPERTY::READ
  );
  gChar->setValue("Hello from ESP32-C6!");
  gService->start();
 
  NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
 
  // Advertise our service UUID
  adv->addServiceUUID(SERVICE_UUID);
 
  // (v2.x) Build advertising + scan-response payloads explicitly
  NimBLEAdvertisementData advData;
  advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported
 
  NimBLEAdvertisementData scanData;
  scanData.setName(DEVICE_NAME); // put the name in scan response
  // you can also add manufacturer data here if you want:
  // std::string mfg = "\x34\x12C6"; scanData.setManufacturerData(mfg);
 
  adv->setAdvertisementData(advData);
  adv->setScanResponseData(scanData);
 
  // Appearance is still supported
  adv->setAppearance(0x0200); // Generic Tag
 
  NimBLEDevice::startAdvertising();
}
 
void setup() {
  Serial.begin(115200);
  while (!Serial) { delay(10); }
  startBLE();
  Serial.println("Advertising as ESP32-C6 Demo. Open nRF Connect -> Scan.");
}
 
void loop() {
  delay(1000);
}

Project Overview

In our project, we will establish an Environmental Sensing Service featuring three distinct characteristics: one for temperature, another for humidity, and a third for pressure.

The specific temperature, humidity, and pressure readings are stored within the values assigned to their respective characteristics. Each characteristic is configured with the notify property, ensuring that the client receives notifications whenever these values undergo a change.

We will adhere to the default UUIDs designated for the Environmental Sensing Profile and its associated characteristics.

To access the default assigned UUID numbers, visit this page and refer to the Assigned Numbers Document (PDF). By searching for the Environmental Sensing Service within the document, you can explore all the authorized characteristics applicable to this service. It's evident that the Environmental Sensing Service supports temperature, humidity, and pressure readings.

There’s a table with the UUIDs for all services. You can see that the UUID for the Environmental Sensing service is 0x181A.

Then, search for the temperature, humidity, and pressure characteristics UUIDs. You’ll find a table with the values for all characteristics. The UUIDs for the temperature, humidity, and pressure are:

  • pressure: 0x2A6D
  • temperature: 0x2A6E
  • humidity: 0x246F

Mobile App

To verify the proper creation of the BLE Server and to receive notifications for temperature, humidity, and pressure, we'll utilize a smartphone application.

Most contemporary smartphones come equipped with BLE capabilities. You can check your smartphone's specifications to confirm its BLE compatibility.

Note: The smartphone can function as either a client or a server. In this context, it will act as the client, establishing a connection with the Sparrow BLE server.

For our testing purposes, we'll employ a free application named nRF Connect for Mobile, developed by Nordic Semi. This app is available on both Android ( Google Play Store) and iOS. To install the app, simply go to the Google Play Store or App Store, search for “nRF Connect for Mobile,” and proceed with the installation.

Sparrow BLE Service

Here are the steps to create an BLE peripheral with an Environmental Sensing BLE service with temperature, humidity, and pressure, characteristics:

  1. Create a BLE device (server) with a name of your choice (we’ll call it ESP32_BME680, but you can call it any other name).
  2. Create an Environmental Sensing service (UUID: 0x181A).
  3. Add characteristics to that service: pressure (0x2A6D), temperature (0x2A6E) and humidity (0x246F)
  4. Add descriptors to the characteristics.
  5. Start the BLE server.
  6. Start advertising so BLE clients can connect and read the characteristics.
  7. Once a connection is established with a client, it will write new values on the characteristics and will notify the client, every time there’s a change.

Copy the following code to the Arduino IDE, modify it and upload it to your board.

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME680.h>
#include <NimBLEDevice.h>
 
/* ============================
   User config
   ============================ */
static const char* DEVICE_NAME = "ESP32-C6 Env";
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define BME680_I2C_ADDR 0x77
static const uint32_t MEAS_INTERVAL_MS   = 2000;  // env update
static const uint32_t BATTERY_INTERVAL_MS = 60000; // battery update
 
/* Device Information strings */
static const char* DIS_MANUFACTURER = "YourBrand";
static const char* DIS_MODEL        = "ESP32-C6 EnvSense";
static const char* DIS_FW_REV       = "1.0.0";
static const char* DIS_SW_REV       = "1.0.0";
 
/* ============================
   UUIDs
   ============================ */
// Environmental Sensing
static NimBLEUUID ESS_UUID ((uint16_t)0x181A);
static NimBLEUUID TEMP_UUID((uint16_t)0x2A6E);
static NimBLEUUID PRES_UUID((uint16_t)0x2A6D);
static NimBLEUUID HUM_UUID ((uint16_t)0x2A6F);
 
// Battery Service
static NimBLEUUID BATT_SVC_UUID((uint16_t)0x180F);
static NimBLEUUID BATT_LVL_UUID((uint16_t)0x2A19);
 
// Device Information Service
static NimBLEUUID DIS_UUID      ((uint16_t)0x180A);
static NimBLEUUID DIS_MFR_UUID  ((uint16_t)0x2A29);
static NimBLEUUID DIS_MODEL_UUID((uint16_t)0x2A24);
static NimBLEUUID DIS_FW_UUID   ((uint16_t)0x2A26);
static NimBLEUUID DIS_SW_UUID   ((uint16_t)0x2A28);
static NimBLEUUID DIS_PNP_UUID  ((uint16_t)0x2A50);
 
/* ============================
   Globals
   ============================ */
Adafruit_BME680 bme; // I2C
 
NimBLEServer* gServer = nullptr;
 
/* Environmental Sensing */
NimBLEService*        gESS      = nullptr;
NimBLECharacteristic* gTempChar = nullptr;
NimBLECharacteristic* gPresChar = nullptr;
NimBLECharacteristic* gHumChar  = nullptr;
 
/* Battery */
NimBLEService*        gBattSvc  = nullptr;
NimBLECharacteristic* gBattChar = nullptr;
 
/* DIS */
NimBLEService*        gDIS      = nullptr;
 
uint32_t lastEnv = 0;
uint32_t lastBatt = 0;
 
/* ============================
   Helpers: GATT encoders
   ============================ */
static void setTempCentiDeg(NimBLECharacteristic* ch, float celsius) {
  int32_t raw = lroundf(celsius * 100.0f);      // sint16, 0.01 °C
  if (raw > 32767) raw = 32767;
  if (raw < -32768) raw = -32768;
  int16_t v = (int16_t)raw;
  uint8_t buf[2] = { (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF) };
  ch->setValue(buf, sizeof(buf));
}
 
static void setHumidityCentiPct(NimBLECharacteristic* ch, float rh) {
  if (rh < 0) rh = 0; if (rh > 100) rh = 100;   // uint16, 0.01 %RH
  uint32_t raw = lroundf(rh * 100.0f);
  if (raw > 0xFFFF) raw = 0xFFFF;
  uint16_t v = (uint16_t)raw;
  uint8_t buf[2] = { (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF) };
  ch->setValue(buf, sizeof(buf));
}
 
static void setPressureDeciPa(NimBLECharacteristic* ch, float pascals) {
  if (pascals < 0) pascals = 0;                 // uint32, 0.1 Pa
  uint64_t raw = llroundf(pascals * 10.0f);
  if (raw > 0xFFFFFFFFULL) raw = 0xFFFFFFFFULL;
  uint32_t v = (uint32_t)raw;
  uint8_t buf[4] = {
    (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF),
    (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 24) & 0xFF)
  };
  ch->setValue(buf, sizeof(buf));
}
 
/* 0x2904 Presentation Format descriptor */
static void addCPF(NimBLECharacteristic* ch, uint8_t format, int8_t exponent, uint16_t unit) {
  // 7 bytes: format, exponent, unit(LE), namespace(1=1), description(2=0)
  uint8_t cpf[7] = {
    format, (uint8_t)exponent,
    (uint8_t)(unit & 0xFF), (uint8_t)((unit >> 8) & 0xFF),
    0x01, 0x00, 0x00
  };
  ch->createDescriptor("2904")->setValue(cpf, sizeof(cpf));
}
 
/* ============================
   Battery level source (stub)
   Replace with ADC/PMIC reading if available.
   ============================ */
static uint8_t getBatteryPercent() {
  // TODO: read your actual battery voltage & map to 0..100
  return 100; // placeholder
}
 
/* ============================
   BLE setup
   ============================ */
void startBLE() {
  NimBLEDevice::init(DEVICE_NAME);
  NimBLEDevice::setPower(ESP_PWR_LVL_P9);
  NimBLEDevice::setSecurityAuth(false, false, true); // no bonding; SC=true
 
  gServer = NimBLEDevice::createServer();
 
  /* --- Environmental Sensing Service --- */
  gESS = gServer->createService(ESS_UUID);
  auto propsRN = (NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
 
  gTempChar = gESS->createCharacteristic(TEMP_UUID, propsRN);
  gPresChar = gESS->createCharacteristic(PRES_UUID, propsRN);
  gHumChar  = gESS->createCharacteristic(HUM_UUID,  propsRN);
 
  // CPF descriptors for units
  const uint8_t GATT_FORMAT_SINT16 = 0x0E;
  const uint8_t GATT_FORMAT_UINT16 = 0x0D;
  const uint8_t GATT_FORMAT_UINT32 = 0x10;
  addCPF(gTempChar, GATT_FORMAT_SINT16, -2, 0x272F); // Celsius
  addCPF(gPresChar, GATT_FORMAT_UINT32, -1, 0x2724); // Pascal, 0.1
  addCPF(gHumChar,  GATT_FORMAT_UINT16, -2, 0x27AD); // %
 
  // Initial values
  setTempCentiDeg(gTempChar, 0.0f);
  setPressureDeciPa(gPresChar, 101325.0f);
  setHumidityCentiPct(gHumChar, 0.0f);
 
  gESS->start();
 
  /* --- Battery Service --- */
  gBattSvc  = gServer->createService(BATT_SVC_UUID);
  gBattChar = gBattSvc->createCharacteristic(BATT_LVL_UUID, propsRN);
  // Optional CPF for % (uint8, exponent 0)
  // format for uint8 = 0x04
  addCPF(gBattChar, 0x04, 0, 0x27AD);
  uint8_t lvl = getBatteryPercent();
  gBattChar->setValue(&lvl, 1);
  gBattSvc->start();
 
  /* --- Device Information Service (all READ-only) --- */
  gDIS = gServer->createService(DIS_UUID);
  gDIS->createCharacteristic(DIS_MFR_UUID,   NIMBLE_PROPERTY::READ)->setValue(DIS_MANUFACTURER);
  gDIS->createCharacteristic(DIS_MODEL_UUID, NIMBLE_PROPERTY::READ)->setValue(DIS_MODEL);
  gDIS->createCharacteristic(DIS_FW_UUID,    NIMBLE_PROPERTY::READ)->setValue(DIS_FW_REV);
  gDIS->createCharacteristic(DIS_SW_UUID,    NIMBLE_PROPERTY::READ)->setValue(DIS_SW_REV);
 
  // PnP ID (7 bytes): Vendor ID Source (1=Bluetooth, 2=USB), Vendor ID (LE16), Product ID (LE16), Product Version (LE16)
  uint8_t pnp[7] = { 0x02, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01 }; // USB, VID 0x1234, PID 0x5678, ver 0x0100
  gDIS->createCharacteristic(DIS_PNP_UUID, NIMBLE_PROPERTY::READ)->setValue(pnp, sizeof(pnp));
 
  gDIS->start();
 
  /* --- Advertising (keeps your explicit payload approach) --- */
  NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
 
  // Advertise primary services (ESS + Battery). DIS is usually not advertised.
  adv->addServiceUUID(ESS_UUID);
  adv->addServiceUUID(BATT_SVC_UUID);
 
  NimBLEAdvertisementData advData;
  advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported
  adv->setAdvertisementData(advData);
 
  NimBLEAdvertisementData scanData;
  scanData.setName(DEVICE_NAME); // put name in scan response
  adv->setScanResponseData(scanData);
 
  // Appearance: Generic Thermometer (helps some UIs)
  adv->setAppearance(0x0300);
 
  NimBLEDevice::startAdvertising();
  Serial.println("Advertising: ESS + Battery + DIS (readable). Open nRF Connect -> Scan.");
}
 
/* ============================
   Sensor setup
   ============================ */
bool startBME680() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  if (!bme.begin(BME680_I2C_ADDR)) {
    Serial.println("[BME680] Not found on 0x77, trying 0x76...");
    if (!bme.begin(0x76)) {
      Serial.println("[BME680] Sensor not found. Check wiring/power.");
      return false;
    }
  }
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(0, 0); // off
  return true;
}
 
/* ============================
   Updates
   ============================ */
void updateEnv() {
  if (!bme.performReading()) {
    Serial.println("[BME680] performReading() failed");
    return;
  }
  float c = bme.temperature;
  float h = bme.humidity;
  float p = bme.pressure;
 
  setTempCentiDeg(gTempChar, c);
  setHumidityCentiPct(gHumChar, h);
  setPressureDeciPa(gPresChar, p);
 
  gTempChar->notify();
  gHumChar->notify();
  gPresChar->notify();
 
  Serial.printf("[ENV] T=%.2f°C  RH=%.2f%%  P=%.1f Pa\n", c, h, p);
}
 
void updateBattery() {
  uint8_t lvl = getBatteryPercent();
  gBattChar->setValue(&lvl, 1);
  gBattChar->notify();
  Serial.printf("[BATT] %u%%\n", lvl);
}
 
/* ============================
   Arduino entry points
   ============================ */
void setup() {
  Serial.begin(115200);
  while (!Serial) { delay(10); }
 
  if (!startBME680()) {
    // Proceed with BLE even if sensor is missing, so you can still connect
  }
  startBLE();
}
 
void loop() {
  uint32_t now = millis();
 
  if (now - lastEnv >= MEAS_INTERVAL_MS) {
    lastEnv = now;
    updateEnv();
  }
 
  if (now - lastBatt >= BATTERY_INTERVAL_MS) {
    lastBatt = now;
    updateBattery();
  }
 
  delay(10);
}

Upload the code to your board. After uploading, open the Serial Monitor, and restart the Sparrow by pressing the RST/EN button.

You should get a data reading messages in the Serial Monitor.

Then, go to your smartphone, open the nRF Connect app from Nordic, and start scanning for new devices. You should find a device called ESP32-C6 Env, this is the BLE server name you defined earlier.

Connect to it. You’ll see that it displays the Environmental Sensing service with the temperature, humidity, and pressure characteristics. Click on the down arrows to activate the notifications.

Then, click on the second icon (the one that looks like a ” mark) at the left to change the format. You can change to unsigned int for all characteristics. You’ll start seeing the temperature, humidity, and pressure values being reported every 2 seconds.

You can also see a Battery Service and a Device Information Service. Battery is hardcoded for now at 100% and device information is also hardcoded.

Web BLE Application

Follow the tutorial here to learn how to create a Web application that connects directly to your Sparrow ESP32 board. You can use the web app just like a normal phone application to send and receive information over BLE from your device.

Web BLE is not currently supported by iOS phones

Build the application in the tutorial and deploy the web page in your GitHub account.

Assignment

Modify the web page and the BLE app to display the BME680 sensor data (temperature, pressure and humidity).

iothings/laboratoare/2025/lab2.1759071905.txt.gz · Last modified: 2025/09/28 18:05 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0