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
This lab demonstrates the core BLE concepts covered in this chapter series:
- Advertising: The ESP32 broadcasts its presence and service UUIDs so smartphones can discover it
- GATT Server: The ESP32 acts as a peripheral hosting services that central devices can read
- Services: Containers for related characteristics (we implement Environmental Sensing and Battery)
- Characteristics: Individual data points with properties (read, notify, indicate)
- Notifications: Server-initiated updates pushed to connected clients without polling
- Connection Parameters: Interval, latency, and timeout settings that affect power and responsiveness
903.5 Wokwi Simulator Environment
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.
- 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
- Add an NTC Temperature Sensor: Click the + button and search for “NTC Temperature Sensor”
- Add a Potentiometer: Click + and search for “Potentiometer” (simulates battery voltage)
- 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
- Click the green Play button to start the simulation
- Watch the Serial Monitor for BLE initialization messages
- The LED will blink slowly indicating the device is advertising
- 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
- nRF Connect (iOS/Android) - Best for detailed GATT exploration
- LightBlue (iOS/Android) - Simple and clean interface
- BLE Scanner (Android) - Lightweight option
Testing Steps:
- Open your BLE scanner app and scan for devices
- Find “IoT-Sensor-Beacon” in the list
- Connect to the device
- Explore the services:
- Environmental Sensing (0x181A): Contains temperature characteristic
- Battery (0x180F): Contains battery level characteristic
- Custom Service: Contains RSSI characteristic
- Enable notifications on the temperature characteristic
- Observe values updating every second
- 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 second903.8 Challenge Exercises
Extend the Environmental Sensing service to include humidity:
- Add a second sensor (DHT22) or use a second potentiometer
- Create a humidity characteristic (UUID: 0x2A6F)
- Humidity format: uint16, resolution 0.01% (so 50.00% = 5000)
- Add notifications for humidity updates
Hint: The humidity characteristic follows the same pattern as temperature.
Add a writable characteristic that lets the connected client change the notification interval:
- Create a custom characteristic with PROPERTY_WRITE
- Accept values 100-10000 (milliseconds)
- Apply the new interval immediately
- Persist the setting (bonus: use EEPROM)
Why this matters: Faster updates drain battery faster. Letting users configure the interval enables power vs. responsiveness tradeoffs.
Create an alert system when battery drops below a threshold:
- Add a new characteristic for alerts (UUID: 0x2A06 - Alert Level)
- When battery < 20%, set alert to “Mild Alert” (value: 1)
- When battery < 10%, set alert to “High Alert” (value: 2)
- Send indication (confirmed notification) for alerts
Why indications?: Critical alerts should be confirmed to ensure the central received them.
903.9 Troubleshooting
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:
- Bluetooth Security: Learn about pairing, bonding, and encryption
- Bluetooth Applications: Real-world BLE deployment case studies
- 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