This hands-on lab walks you through building a complete BLE sensor beacon on ESP32, covering GATT service design, characteristic notifications for real-time sensor data, and connection event handling. Using the browser-based Wokwi simulator, you implement temperature and battery monitoring over BLE without physical hardware.
Key Concepts
Wokwi Simulator: Browser-based ESP32/Arduino simulator supporting BLE emulation, allowing firmware development without physical hardware
ESP-IDF NimBLE Stack: Lightweight BLE host stack for ESP32 with lower RAM footprint than Bluedroid; preferred for IoT sensor firmware
BLE Peripheral Role: Device that advertises services and responds to connection requests from a central; the “server” in GATT terminology
BLE Central Role: Device that scans for peripherals and initiates connections; the “client” in GATT terminology (e.g., smartphone app)
nRF Connect App: Nordic Semiconductor’s BLE debugging app for Android/iOS; displays all GATT services/characteristics and allows direct read/write
GATT Notification: Server-initiated data push to subscribed clients without requiring a read request; requires CCCD to be written to 0x0001
ADV_IND: Undirected connectable advertising PDU; broadcast on all three advertising channels; the most common advertising type for IoT peripherals
Descriptor: GATT metadata attached to a characteristic; common examples include CCCD (0x2902) for notification control and CPF (0x2904) for unit/format description
Minimum Viable Understanding
Build a BLE sensor beacon on the ESP32 that advertises standard and custom GATT services (Environmental Sensing 0x181A, Battery 0x180F), pushes real-time temperature and battery readings via notifications, and handles connect/disconnect events. The Wokwi browser simulator lets you complete the entire lab without physical hardware.
24.1 Learning Objectives
By completing this lab, you will be able to:
Configure BLE advertising with custom service UUIDs and device names on the ESP32
Implement GATT services including custom temperature and standard battery level characteristics
Apply BLE notifications to push sensor data to connected clients without polling
Construct connection event handlers for connect, disconnect, and advertising restart sequences
Analyze RSSI values (Received Signal Strength Indicator) to estimate proximity between devices
Design power-efficient BLE peripherals by calculating and comparing advertising interval tradeoffs
Distinguish between standard Bluetooth SIG UUIDs and custom 128-bit UUIDs and justify when each applies
For Beginners: BLE Sensor Beacon Lab
In this lab, you will build a BLE beacon – a small device that periodically broadcasts sensor data (like temperature) to any nearby phone or computer that is listening. Think of it as building a tiny radio station that broadcasts sensor readings. It is one of the simplest and most practical BLE projects you can build.
Sensor Squad: Building Our First BLE Beacon
“Today we build something real!” said Max the Microcontroller excitedly, holding up an ESP32 board. “We are going to create a BLE sensor beacon that broadcasts temperature readings to any phone nearby. It is like Sammy getting his own tiny radio station!”
Sammy the Sensor was thrilled. “How does it work?” Max explained, “First, we set up BLE advertising – that is like putting up a sign that says ‘Temperature Sensor Here!’ Then we create a GATT service with a temperature characteristic. When a phone connects, we push temperature updates using notifications – no need for the phone to keep asking.”
“The coolest part is notifications,” said Lila the LED. “Instead of the phone polling ‘What is the temperature? What is the temperature?’ over and over, our beacon just pushes updates whenever the value changes. It is way more efficient and saves battery on both sides.”
Bella the Battery added practical advice. “Pay attention to the advertising interval. Broadcasting every 100 milliseconds drains me fast, but every 1000 milliseconds gives a good balance between discovery speed and battery life. For a sensor that updates every few seconds, there is no reason to advertise constantly.”
Putting Numbers to It
The advertising interval directly affects battery life. The average current draw is:
where \(I_{active}\) is active current (12mA), \(T_{adv}\) is advertising duration (3ms), \(T_{interval}\) is the advertising interval, and \(I_{sleep}\) is sleep current (5µA).
Example: For a CR2032 battery (220mAh) with 100ms vs 1000ms intervals: - 100ms interval: \(I_{avg} = 12 \times \frac{0.003}{0.1} + 0.005 \times 0.97 = 0.365\) mA → 25 days battery life - 1000ms interval: \(I_{avg} = 12 \times \frac{0.003}{1.0} + 0.005 \times 0.997 = 0.041\) mA → 224 days battery life
The 10x slower advertising rate gives approximately 9x longer battery life with minimal impact on discoverability.
Try It: BLE Advertising Interval Battery Life Calculator
Adjust the advertising interval and see how it affects battery life for a CR2032-powered BLE beacon.
Familiarity with BLE concepts from this chapter series (GATT, services, characteristics, advertising)
No physical hardware required (browser-based simulation)
24.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)
24.4 Key BLE Concepts in This Lab
BLE Architecture Overview
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
24.5 Wokwi Simulator Environment
About 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.
Simulator 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
24.6 Step-by-Step Instructions
24.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
24.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
24.6.3 Step 3: Copy the BLE Sensor Beacon Code
Copy the following code into the Wokwi code editor (replace any existing code). The code is split into key sections below:
Includes and Configuration:
#include <BLEDevice.h>#include <BLEServer.h>#include <BLEUtils.h>#include <BLE2902.h>#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// Standard Bluetooth SIG UUIDs#define ENVIRONMENTAL_SENSING_SERVICE_UUID "181A"#define TEMPERATURE_CHAR_UUID "2A6E"#define BATTERY_SERVICE_UUID "180F"#define BATTERY_LEVEL_CHAR_UUID "2A19"BLEServer* pServer =nullptr;BLECharacteristic* pTemperatureChar =nullptr;BLECharacteristic* pBatteryChar =nullptr;bool deviceConnected =false;
Enable notifications on the temperature characteristic
Observe values updating every second
Adjust the potentiometer in Wokwi to change the battery reading
24.7 Understanding the Code
24.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
24.7.2 Characteristic Properties
BLECharacteristic::PROPERTY_READ |// Client can read valueBLECharacteristic::PROPERTY_NOTIFY // Server can push updates
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.
Challenge 3: Add Low Battery Alert
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.
24.9 Troubleshooting
Common 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
24.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
24.10.1 Knowledge Check: GATT Notifications
24.10.2 Knowledge Check: BLE Data Format
Common Pitfalls
1. Not Enabling BLE in Wokwi Project Configuration
Wokwi requires explicit BLE component enablement in the ESP-IDF sdkconfig (CONFIG_BT_ENABLED=y, CONFIG_BT_NIMBLE_ENABLED=y). Projects copied from non-BLE templates will compile but the BLE stack will not initialize, producing silent failures. Always start from a BLE-specific Wokwi template or verify sdkconfig BLE settings before debugging.
2. Hardcoding UUIDs That Conflict With Standard Services
Using 16-bit UUIDs like 0x1800 (Generic Access) or 0x180A (Device Information) for custom services causes BLE clients to misinterpret the service as a standard service with incorrect characteristics. Custom services must use 128-bit UUIDs generated with uuidgen (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). 16-bit UUIDs are reserved for Bluetooth SIG adopted services.
3. Forgetting to Start Advertising After Service Registration
In NimBLE and Bluedroid, registering GATT services does not automatically start advertising. The ble_gap_adv_start() call (NimBLE) or esp_ble_gap_start_advertising() call (Bluedroid) must be made explicitly, typically in the BLE host sync callback. A device that initializes the stack but never calls start advertising will be invisible to scanners.
4. Setting Too-Large a Notification Payload Without MTU Exchange
Sending a 100-byte notification without first negotiating a larger ATT MTU will cause the stack to silently truncate the payload to 20 bytes (default MTU 23 - 3 header bytes). Always perform ATT MTU exchange after connection (NimBLE: ble_gattc_exchange_mtu()) before sending large notifications, and verify the negotiated MTU with ble_att_mtu().
24.11 What’s Next
Having completed this BLE sensor beacon lab, you can deepen your Bluetooth knowledge through the following chapters:
Connection parameters, sleep modes, and adaptive notification strategies
Extend battery life beyond the 224-day baseline calculated in this lab
Worked Example: Optimizing BLE Notification Interval for Battery Life vs Responsiveness
Scenario: Your environmental sensor beacon from the lab transmits temperature and battery level every 1 second. Users complain the coin cell battery (CR2032, 220mAh) only lasts 3 months instead of the advertised 12+ months.
Given:
Current configuration: Notify every 1000ms (1 second)
BLE radio active current: 12mA for 3ms per transmission
Sleep current: 5µA (ultra-low power mode)
Temperature changes slowly (0.1°C per minute typical)
Battery level changes 1% per week
Analysis:
Calculate current power consumption (1-second interval):
Duty cycle: 3ms active / 1000ms period = 0.3%
Active power: 12mA × 0.003 = 0.036mA
Sleep power: 0.005mA × 0.997 = 0.005mA
Average current: 0.036 + 0.005 = 0.041mA
Battery life: 220mAh / 0.041mA = 5,366 hours ≈ 224 days (7.5 months)
Identify optimization opportunity:
Temperature changes: 0.1°C per minute
Current sampling: 60 readings per minute
Useful readings: 1 per minute (99% redundant!)
Battery level changes: 1% per week
Current sampling: 604,800 readings per week
Useful readings: 1 per day would suffice
Design adaptive notification strategy:
Temperature: Notify every 30 seconds (not every 1 second)
- Still captures 0.1°C/min changes with 0.05°C granularity
Battery level: Notify every 10 minutes (not every 1 second)
- More than sufficient for 1%/week change rate
- Create separate characteristic to avoid tight coupling
Calculate power savings:
New duty cycle:
- Temperature: 3ms / 30,000ms = 0.01% (30-second interval)
- Battery: 3ms / 600,000ms = 0.0005% (10-minute interval)
- Combined: 0.0105% vs 0.3% original
New average current:
- Active: 12mA × 0.000105 = 0.00126mA
- Sleep: 0.005mA × 0.999895 = 0.00499mA
- Total: 0.00625mA (was 0.041mA)
New battery life: 220mAh / 0.00625mA = 35,200 hours ≈ 1,467 days (4+ years!)
Implementation with change-based notification (bonus optimization):
// Add threshold-based updatefloat lastNotifiedTemp =0;constfloat TEMP_THRESHOLD =0.5;// Only notify on 0.5°C changevoid loop(){if(deviceConnected){float currentTemp = readTemperature()/100.0;// Only notify if significant change OR 30 seconds elapsedif(abs(currentTemp - lastNotifiedTemp)> TEMP_THRESHOLD ||(millis()- lastNotifyTime >=30000)){// Send notification pTemperatureChar->notify(); lastNotifiedTemp = currentTemp; lastNotifyTime = millis();}} delay(1000);// Check every second, notify selectively}
Result: By matching notification intervals to actual data change rates, battery life increased from 7.5 months to 4+ years (6.5× improvement) with ZERO loss of useful information. The user experience improved because fewer unnecessary notifications reduced smartphone battery drain.
Key Insight: The fastest notification interval is rarely the best. Match your notification rate to your data’s natural change rate. For slow-changing values, event-driven notifications (only send on significant change) are more efficient than periodic updates.
Interactive Quiz: Match Lab Concepts
Interactive Quiz: Sequence the Lab Steps
🏷️ Label the Diagram
💻 Code Challenge
24.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