903  Bluetooth Hands-On Lab: BLE Sensor Beacon

903.1 Learning Objectives

By completing this lab, you will be able to:

  • Configure BLE advertising with custom service UUIDs and device names
  • Implement GATT services including custom temperature and standard battery level characteristics
  • Use BLE notifications to push sensor data to connected clients without polling
  • Handle connection events including connect, disconnect, and MTU negotiation
  • Monitor RSSI (Received Signal Strength Indicator) for proximity detection
  • Design power-efficient BLE peripherals by understanding advertising intervals and connection parameters

903.2 Prerequisites

  • Basic understanding of Arduino/C++ syntax
  • Familiarity with BLE concepts from this chapter series (GATT, services, characteristics, advertising)
  • No physical hardware required (browser-based simulation)

903.3 Components Used

Component Purpose Connection
ESP32 DevKit BLE-capable microcontroller Main board
NTC Thermistor (or DHT22) Temperature sensing GPIO 34 (analog)
LED Connection status indicator GPIO 2 (built-in)
Potentiometer Simulated battery voltage GPIO 35 (analog)

903.4 Key BLE Concepts in This Lab

NoteBLE Architecture Overview

This lab demonstrates the core BLE concepts covered in this chapter series:

  1. Advertising: The ESP32 broadcasts its presence and service UUIDs so smartphones can discover it
  2. GATT Server: The ESP32 acts as a peripheral hosting services that central devices can read
  3. Services: Containers for related characteristics (we implement Environmental Sensing and Battery)
  4. Characteristics: Individual data points with properties (read, notify, indicate)
  5. Notifications: Server-initiated updates pushed to connected clients without polling
  6. Connection Parameters: Interval, latency, and timeout settings that affect power and responsiveness

903.5 Wokwi Simulator Environment

NoteAbout Wokwi

Wokwi is a free online simulator for Arduino, ESP32, and other microcontrollers. It allows you to build and test IoT projects entirely in your browser without purchasing hardware. The ESP32 simulator includes BLE support, making it ideal for learning BLE concepts.

Launch the simulator below to get started. The default project includes an ESP32 - you will add components and code as you progress through the lab.

TipSimulator Tips
  • Click on the ESP32 to see available pins
  • Use the + button to add components (search for “NTC Temperature Sensor” or “Potentiometer”)
  • Connect wires by clicking on pins
  • The Serial Monitor shows BLE events and debug output
  • Press the green Play button to run your code
  • Use the nRF Connect app on your phone (or LightBlue) to connect to the simulated BLE device

903.6 Step-by-Step Instructions

903.6.1 Step 1: Set Up the Circuit

  1. Add an NTC Temperature Sensor: Click the + button and search for “NTC Temperature Sensor”
  2. Add a Potentiometer: Click + and search for “Potentiometer” (simulates battery voltage)
  3. Wire the components to ESP32:
    • NTC VCC -> ESP32 3.3V
    • NTC OUT -> ESP32 GPIO 34 (ADC)
    • NTC GND -> ESP32 GND
    • Potentiometer VCC -> ESP32 3.3V
    • Potentiometer Signal -> ESP32 GPIO 35 (ADC)
    • Potentiometer GND -> ESP32 GND

903.6.2 Step 2: Understanding the Code Structure

Before copying the code, understand the BLE components we will implement:

GATT Server Structure:
|
+-- Environmental Sensing Service (0x181A)
|     +-- Temperature Characteristic (0x2A6E)
|           Properties: Read, Notify
|
+-- Battery Service (0x180F)
|     +-- Battery Level Characteristic (0x2A19)
|           Properties: Read, Notify
|
+-- Custom Service (128-bit UUID)
      +-- RSSI Characteristic (128-bit UUID)
            Properties: Read, Notify

903.6.3 Step 3: Copy the BLE Sensor Beacon Code

Copy the following code into the Wokwi code editor (replace any existing code):

/*
 * BLE Sensor Beacon Lab
 *
 * This code demonstrates core BLE concepts:
 * - Advertising with custom device name and service UUIDs
 * - GATT server with Environmental Sensing and Battery services
 * - Notifications for real-time sensor updates
 * - Connection handling and RSSI monitoring
 *
 * Compatible with: ESP32 DevKit, Wokwi Simulator
 * Test with: nRF Connect, LightBlue, or any BLE scanner app
 */

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

// ========== PIN DEFINITIONS ==========
#define TEMP_PIN 34          // NTC thermistor analog input
#define BATTERY_PIN 35       // Potentiometer (simulates battery voltage)
#define LED_PIN 2            // Built-in LED for connection status

// ========== BLE UUIDs ==========
// Standard Bluetooth SIG UUIDs (16-bit short form)
#define ENVIRONMENTAL_SENSING_SERVICE_UUID  "181A"
#define TEMPERATURE_CHAR_UUID               "2A6E"
#define BATTERY_SERVICE_UUID                "180F"
#define BATTERY_LEVEL_CHAR_UUID             "2A19"

// Custom service UUID for additional features (128-bit)
#define CUSTOM_SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define RSSI_CHAR_UUID             "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// ========== BLE OBJECTS ==========
BLEServer* pServer = nullptr;
BLECharacteristic* pTemperatureChar = nullptr;
BLECharacteristic* pBatteryChar = nullptr;
BLECharacteristic* pRssiChar = nullptr;

// ========== STATE VARIABLES ==========
bool deviceConnected = false;
bool oldDeviceConnected = false;
uint32_t lastNotifyTime = 0;
const uint32_t NOTIFY_INTERVAL = 1000;  // Send notifications every 1 second

int connectionCount = 0;
int8_t lastRssi = 0;

// ========== BLE CALLBACKS ==========

// Server callbacks: Handle connect/disconnect events
class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        connectionCount++;
        digitalWrite(LED_PIN, HIGH);  // LED on when connected

        Serial.println("=====================================");
        Serial.println("CLIENT CONNECTED!");
        Serial.print("Total connections since boot: ");
        Serial.println(connectionCount);
        Serial.println("=====================================");
    }

    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        digitalWrite(LED_PIN, LOW);  // LED off when disconnected

        Serial.println("=====================================");
        Serial.println("CLIENT DISCONNECTED");
        Serial.println("Restarting advertising...");
        Serial.println("=====================================");
    }
};

// Characteristic callbacks: Handle read/write requests
class TemperatureCallbacks : public BLECharacteristicCallbacks {
    void onRead(BLECharacteristic* pCharacteristic) {
        Serial.println("Temperature characteristic read by client");
    }
};

class BatteryCallbacks : public BLECharacteristicCallbacks {
    void onRead(BLECharacteristic* pCharacteristic) {
        Serial.println("Battery level characteristic read by client");
    }
};

// ========== SENSOR FUNCTIONS ==========

// Read temperature from NTC thermistor
// Returns temperature in Celsius (scaled for BLE: actual * 100)
int16_t readTemperature() {
    int rawValue = analogRead(TEMP_PIN);

    // Convert ADC reading to temperature
    // NTC thermistor formula (simplified for simulation)
    float voltage = rawValue * (3.3 / 4095.0);
    float resistance = (3.3 - voltage) * 10000.0 / voltage;

    // Simplified temperature calculation
    float tempC = 25.0 + (resistance - 10000.0) / -200.0;

    // Clamp to reasonable range
    if (tempC < -40) tempC = -40;
    if (tempC > 85) tempC = 85;

    // BLE Environmental Sensing uses sint16 with resolution 0.01 degrees
    return (int16_t)(tempC * 100);
}

// Read simulated battery level (0-100%)
uint8_t readBatteryLevel() {
    int rawValue = analogRead(BATTERY_PIN);
    // Map ADC range (0-4095) to battery percentage (0-100)
    uint8_t percentage = map(rawValue, 0, 4095, 0, 100);
    return percentage;
}

// ========== BLE SETUP ==========
void setupBLE() {
    Serial.println("Initializing BLE...");

    // Initialize BLE with device name
    BLEDevice::init("IoT-Sensor-Beacon");

    // Create BLE Server
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());

    // ===== Environmental Sensing Service =====
    BLEService* pEnvService = pServer->createService(ENVIRONMENTAL_SENSING_SERVICE_UUID);

    // Temperature Characteristic
    // Properties: Read (client can request value) + Notify (server pushes updates)
    pTemperatureChar = pEnvService->createCharacteristic(
        TEMPERATURE_CHAR_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY
    );

    // Add Client Characteristic Configuration Descriptor (CCCD)
    // Required for notifications - allows client to enable/disable
    pTemperatureChar->addDescriptor(new BLE2902());
    pTemperatureChar->setCallbacks(new TemperatureCallbacks());

    // ===== Battery Service =====
    BLEService* pBatteryService = pServer->createService(BATTERY_SERVICE_UUID);

    // Battery Level Characteristic
    pBatteryChar = pBatteryService->createCharacteristic(
        BATTERY_LEVEL_CHAR_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY
    );
    pBatteryChar->addDescriptor(new BLE2902());
    pBatteryChar->setCallbacks(new BatteryCallbacks());

    // ===== Custom Service for RSSI =====
    BLEService* pCustomService = pServer->createService(CUSTOM_SERVICE_UUID);

    // RSSI Characteristic (custom - shows signal strength)
    pRssiChar = pCustomService->createCharacteristic(
        RSSI_CHAR_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY
    );
    pRssiChar->addDescriptor(new BLE2902());

    // Start all services
    pEnvService->start();
    pBatteryService->start();
    pCustomService->start();

    // ===== Configure Advertising =====
    BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();

    // Add service UUIDs to advertisement packet
    pAdvertising->addServiceUUID(ENVIRONMENTAL_SENSING_SERVICE_UUID);
    pAdvertising->addServiceUUID(BATTERY_SERVICE_UUID);

    // Configure advertising parameters
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);  // 7.5ms units
    pAdvertising->setMaxPreferred(0x12);

    // Start advertising
    BLEDevice::startAdvertising();

    Serial.println("BLE Sensor Beacon started!");
    Serial.println("Device Name: IoT-Sensor-Beacon");
    Serial.println("Services:");
    Serial.println("  - Environmental Sensing (0x181A)");
    Serial.println("  - Battery (0x180F)");
    Serial.println("  - Custom RSSI Service");
    Serial.println("");
    Serial.println("Use nRF Connect or LightBlue app to connect");
}

// ========== ARDUINO SETUP ==========
void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("");
    Serial.println("========================================");
    Serial.println("  BLE Sensor Beacon Lab");
    Serial.println("  Bluetooth Fundamentals Chapter");
    Serial.println("========================================");
    Serial.println("");

    // Configure pins
    pinMode(LED_PIN, OUTPUT);
    pinMode(TEMP_PIN, INPUT);
    pinMode(BATTERY_PIN, INPUT);

    // Blink LED to indicate startup
    for (int i = 0; i < 3; i++) {
        digitalWrite(LED_PIN, HIGH);
        delay(200);
        digitalWrite(LED_PIN, LOW);
        delay(200);
    }

    // Initialize BLE
    setupBLE();
}

// ========== MAIN LOOP ==========
void loop() {
    // Handle connection state changes
    if (deviceConnected != oldDeviceConnected) {
        if (!deviceConnected) {
            // Client disconnected - restart advertising
            delay(500);
            pServer->startAdvertising();
            Serial.println("Advertising restarted");
        }
        oldDeviceConnected = deviceConnected;
    }

    // Send notifications when connected
    if (deviceConnected) {
        uint32_t currentTime = millis();

        if (currentTime - lastNotifyTime >= NOTIFY_INTERVAL) {
            lastNotifyTime = currentTime;

            // Read and send temperature
            int16_t tempValue = readTemperature();
            float tempCelsius = tempValue / 100.0;

            // Set characteristic value and notify
            // BLE uses little-endian byte order
            uint8_t tempData[2];
            tempData[0] = tempValue & 0xFF;
            tempData[1] = (tempValue >> 8) & 0xFF;
            pTemperatureChar->setValue(tempData, 2);
            pTemperatureChar->notify();

            // Read and send battery level
            uint8_t batteryLevel = readBatteryLevel();
            pBatteryChar->setValue(&batteryLevel, 1);
            pBatteryChar->notify();

            // Update RSSI (simulated in Wokwi)
            int8_t rssi = -50 - (random(0, 30));
            pRssiChar->setValue((uint8_t*)&rssi, 1);
            pRssiChar->notify();

            // Print to serial monitor
            Serial.println("--- Notification Sent ---");
            Serial.print("Temperature: ");
            Serial.print(tempCelsius, 2);
            Serial.println(" C");
            Serial.print("Battery: ");
            Serial.print(batteryLevel);
            Serial.println(" %");
            Serial.print("RSSI: ");
            Serial.print(rssi);
            Serial.println(" dBm");
            Serial.println("");
        }
    } else {
        // Not connected - blink LED slowly to indicate advertising
        static uint32_t lastBlinkTime = 0;
        static bool ledState = false;

        if (millis() - lastBlinkTime >= 1000) {
            lastBlinkTime = millis();
            ledState = !ledState;
            digitalWrite(LED_PIN, ledState);
        }
    }

    delay(10);
}

903.6.4 Step 4: Run the Simulation

  1. Click the green Play button to start the simulation
  2. Watch the Serial Monitor for BLE initialization messages
  3. The LED will blink slowly indicating the device is advertising
  4. In a separate browser tab or on your phone, use a BLE scanner app to find “IoT-Sensor-Beacon”

903.6.5 Step 5: Connect and Test with a BLE App

TipRecommended BLE Apps
  • nRF Connect (iOS/Android) - Best for detailed GATT exploration
  • LightBlue (iOS/Android) - Simple and clean interface
  • BLE Scanner (Android) - Lightweight option

Testing Steps:

  1. Open your BLE scanner app and scan for devices
  2. Find “IoT-Sensor-Beacon” in the list
  3. Connect to the device
  4. Explore the services:
    • Environmental Sensing (0x181A): Contains temperature characteristic
    • Battery (0x180F): Contains battery level characteristic
    • Custom Service: Contains RSSI characteristic
  5. Enable notifications on the temperature characteristic
  6. Observe values updating every second
  7. Adjust the potentiometer in Wokwi to change the battery reading

903.7 Understanding the Code

903.7.1 UUID Selection

// Standard UUIDs (defined by Bluetooth SIG)
#define ENVIRONMENTAL_SENSING_SERVICE_UUID  "181A"  // Well-known service
#define TEMPERATURE_CHAR_UUID               "2A6E"  // Standard temperature format

// Custom UUID (generated for your application)
#define CUSTOM_SERVICE_UUID  "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
  • Standard UUIDs allow apps to automatically understand your data format
  • Custom UUIDs are used for proprietary features specific to your device

903.7.2 Characteristic Properties

BLECharacteristic::PROPERTY_READ |    // Client can read value
BLECharacteristic::PROPERTY_NOTIFY    // Server can push updates
Property Description Use Case
READ Client can request current value On-demand data retrieval
WRITE Client can set value Configuration, commands
NOTIFY Server pushes updates (unconfirmed) Real-time sensor data
INDICATE Server pushes updates (confirmed) Critical alerts

903.7.3 CCCD (Client Characteristic Configuration Descriptor)

pTemperatureChar->addDescriptor(new BLE2902());

The BLE2902 descriptor allows clients to enable/disable notifications. Without this, the notify property would not work.

903.7.4 Data Format for BLE

// BLE uses little-endian byte order
uint8_t tempData[2];
tempData[0] = tempValue & 0xFF;         // Low byte first
tempData[1] = (tempValue >> 8) & 0xFF;  // High byte second

903.8 Challenge Exercises

CautionChallenge 1: Add Humidity Sensing

Extend the Environmental Sensing service to include humidity:

  1. Add a second sensor (DHT22) or use a second potentiometer
  2. Create a humidity characteristic (UUID: 0x2A6F)
  3. Humidity format: uint16, resolution 0.01% (so 50.00% = 5000)
  4. Add notifications for humidity updates

Hint: The humidity characteristic follows the same pattern as temperature.

CautionChallenge 2: Implement Configurable Notification Interval

Add a writable characteristic that lets the connected client change the notification interval:

  1. Create a custom characteristic with PROPERTY_WRITE
  2. Accept values 100-10000 (milliseconds)
  3. Apply the new interval immediately
  4. Persist the setting (bonus: use EEPROM)

Why this matters: Faster updates drain battery faster. Letting users configure the interval enables power vs. responsiveness tradeoffs.

CautionChallenge 3: Add Low Battery Alert

Create an alert system when battery drops below a threshold:

  1. Add a new characteristic for alerts (UUID: 0x2A06 - Alert Level)
  2. When battery < 20%, set alert to “Mild Alert” (value: 1)
  3. When battery < 10%, set alert to “High Alert” (value: 2)
  4. Send indication (confirmed notification) for alerts

Why indications?: Critical alerts should be confirmed to ensure the central received them.

903.9 Troubleshooting

WarningCommon Issues and Solutions

Device not appearing in BLE scan:

  • Ensure advertising is started after service setup
  • Check that service UUIDs are added to advertising packet
  • Some apps filter by service UUID - try “Show all devices”

Notifications not working:

  • Verify CCCD (BLE2902) descriptor is added to characteristic
  • Client must write 0x0001 to CCCD to enable notifications
  • Check if notification is called: pCharacteristic->notify()

Connection drops immediately:

  • Check connection interval settings (too aggressive for some clients)
  • Ensure delay() in loop is not too long (>supervision timeout)
  • Some phones disconnect if no services are discovered quickly

Values appear incorrect:

  • Verify byte order (BLE uses little-endian)
  • Check value scaling (temperature uses 0.01 resolution)
  • Ensure characteristic value size matches data type

903.10 Lab Summary

In this lab, you built a complete BLE sensor beacon that demonstrates:

Concept Implementation
BLE Advertising Device broadcasts name and service UUIDs for discovery
GATT Server ESP32 hosts services that clients can connect to
Standard Services Environmental Sensing (0x181A) and Battery (0x180F)
Characteristics Temperature (0x2A6E) and Battery Level (0x2A19) with proper formats
Notifications Real-time sensor updates pushed to connected clients
Connection Handling Callbacks for connect/disconnect events
RSSI Monitoring Signal strength for proximity estimation

903.11 What’s Next

To deepen your BLE knowledge:

  1. Bluetooth Security: Learn about pairing, bonding, and encryption
  2. Bluetooth Applications: Real-world BLE deployment case studies
  3. Try on Real Hardware: Flash this code to an actual ESP32 and test with your phone

903.12 Summary

This hands-on lab demonstrated practical BLE development:

  • GATT service design with standard and custom UUIDs
  • Characteristic properties (Read, Notify) for different access patterns
  • Connection event handling for robust peripheral behavior
  • Data formatting with little-endian byte order for BLE compliance
  • Advertising configuration for device discovery
  • Power considerations through notification intervals