23 BLE Hands-On Labs
- BLE Lab Setup: Minimum requirements: ESP32 development board (with BLE), USB cable, nRF Connect app on smartphone, and IDE (VS Code + ESP-IDF or Arduino IDE)
- Service Discovery Procedure: Client sends ATT Read By Group Type Request to enumerate all primary services, then ATT Read By Type Request to enumerate characteristics within each service
- Characteristic Handle: Unique 16-bit identifier assigned by the GATT server to each attribute (service declaration, characteristic declaration, value, descriptor)
- Notification Enable Sequence: Client writes 0x0001 to the CCCD (at characteristic handle + 1 or + 2) to subscribe; server calls ble_gattc_notify_custom() to push data
- OTA DFU Lab: Firmware update over BLE using Nordic NRF DFU protocol (Image Info service + Packet characteristic); requires bootloader in flash, DFU trigger, and nRF Connect DFU function
- BLE Traffic Capture Lab: Using nRF Sniffer USB dongle with Wireshark dissector plugin to capture and decode BLE advertising + connection packets for debugging
- Power Profiling Lab: Using Nordic PPK2 (Power Profiler Kit 2) or Otii Arc to measure BLE duty cycle power consumption, identifying wakeup overhead and connection event energy
- BLE Latency Measurement: GPIO toggle at TX and RX points + logic analyzer to measure actual BLE notification latency from sensor event to central receipt
Building real BLE applications requires combining GATT service design, notification-based data push, and power-aware connection management. Standard Bluetooth SIG profiles (like Heart Rate Service) ensure interoperability across platforms, while custom services provide flexibility for unique IoT use cases.
23.1 Learning Objectives
By the end of this chapter, you will be able to:
- Implement Complete BLE Projects: Construct end-to-end BLE sensor applications using ESP32 and the Arduino BLE library
- Configure Standard GATT Services: Implement Heart Rate Service (0x180D) and other Bluetooth SIG specifications with correct characteristic properties
- Develop Python Dashboards: Build real-time monitoring applications using the bleak library with asynchronous notification handling
- Apply Indoor Positioning: Configure BLE beacons and apply trilateration algorithms to estimate indoor locations from RSSI measurements
- Analyze Mesh Network Behavior: Evaluate BLE mesh message flooding, TTL propagation, and relay node impact on network traffic
What you’ll build: Four complete projects that demonstrate real-world BLE applications.
Before starting:
- Review BLE Code Examples for basic patterns
- Review BLE Python Implementations for library usage
- Have hardware ready: ESP32 dev boards, sensors, or use Wokwi simulator
Time commitment: Each lab takes 1-3 hours depending on your experience level.
“Four amazing projects await!” announced Max the Microcontroller, spreading out ESP32 boards and sensors on the workbench. “We are building a heart rate monitor, a real-time Python dashboard, an indoor positioning system, and a mesh network simulator. Each project teaches different BLE skills.”
Sammy the Sensor was drawn to the heart rate monitor. “That uses a standard Bluetooth SIG profile, right?” Max nodded. “Exactly! The Heart Rate Service has a UUID of 0x180D, and every BLE-capable phone already knows how to read it. By using standard profiles, your device works with any fitness app without custom code on the phone side.”
“I am excited about indoor positioning,” said Lila the LED. “You place BLE beacons at known locations, measure signal strength from each one, and use trilateration to estimate where a device is. It is like GPS, but indoors! The tricky part is that RSSI varies a lot due to reflections and obstacles, so you need filtering and calibration.”
Bella the Battery reminded everyone about practical constraints. “When building the mesh network simulator, remember that Bluetooth Mesh uses managed flooding – every relay node rebroadcasts every message. With 50 nodes and a TTL of 7, a single message generates hundreds of transmissions. That is great for reliability but terrible for my battery life, so choose your relay nodes carefully.”
In Bluetooth Mesh, the number of transmissions per message is governed by the Time-To-Live (TTL) and relay density:
\[N_{transmissions} \approx N_{relays} \times (1 + R_{avg})^{TTL}\]
where \(N_{relays}\) is the number of relay nodes, \(R_{avg}\) is the average number of neighbors per node, and \(TTL\) is the message hop limit.
Example: A 50-node mesh network with average connectivity of 3 neighbors per node: - TTL = 7 (maximum hops) - Relay nodes: 40 (80% of nodes) - First hop: 1 originator → 3 relays = 3 transmissions - Each relay reaches ~3 new nodes → exponential growth - Upper bound: \(40 \times 3^{7} = 40 \times 2187 = 87,480\) transmissions (worst case with no loop prevention) - With mesh cache (prevents re-relay): ~120-300 transmissions (observed in practice)
Even with loop prevention, a single message generates 100+ radio events. For battery-powered nodes, limit relay capability to mains-powered devices.
23.2 Prerequisites
Before starting these labs:
- BLE Code Examples and Simulators: Basic GATT server/client patterns
- BLE Python Implementations: Scanner and proximity detection code
- Hardware: ESP32 development board(s) or Wokwi simulator access
23.3 Lab 1: ESP32 BLE Heart Rate Monitor
Objective: Build a BLE heart rate monitor using standard Heart Rate Service (0x180D).
Materials:
- ESP32 development board
- Heart rate sensor (MAX30102) or simulate with potentiometer
- Breadboard and wires
- nRF Connect app (Android/iOS)
Circuit Diagram:
MAX30102 ESP32
-------- -----
VIN ------> 3.3V
GND ------> GND
SDA ------> GPIO 21 (I2C SDA)
SCL ------> GPIO 22 (I2C SCL)
Complete Code:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>
// Standard Heart Rate Service and Measurement UUIDs
#define SERVICE_UUID "0000180d-0000-1000-8000-00805f9b34fb"
#define CHARACTERISTIC_UUID "00002a37-0000-1000-8000-00805f9b34fb"
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
class ServerCB : public BLEServerCallbacks {
void onConnect(BLEServer* s) { deviceConnected = true; }
void onDisconnect(BLEServer* s) { deviceConnected = false; }
};
void setup() {
Serial.begin(115200);
BLEDevice::init("HR-Monitor-ESP32");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCB());
BLEService* pSvc = pServer->createService(SERVICE_UUID);
pCharacteristic = pSvc->createCharacteristic(CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristic->addDescriptor(new BLE2902());
pSvc->start();
BLEDevice::getAdvertising()->addServiceUUID(SERVICE_UUID);
BLEDevice::getAdvertising()->start();
}
void loop() {
if (deviceConnected) {
// HR Measurement: Byte 0 = flags, Byte 1 = BPM
uint8_t hrData[2] = {0x00, (uint8_t)random(60, 100)};
pCharacteristic->setValue(hrData, 2);
pCharacteristic->notify();
delay(1000);
} else {
static bool wasConnected = false;
if (wasConnected) { pServer->startAdvertising(); wasConnected = false; }
}
}Expected Output (Serial Monitor):
BLE Heart Rate Monitor Starting...
BLE Heart Rate Monitor ready!
Open nRF Connect app to connect
Client connected
Heart Rate: 72 BPM
Heart Rate: 78 BPM
Heart Rate: 65 BPM
Client disconnected
Advertising restarted
Testing with nRF Connect:
- Open nRF Connect app
- Scan for devices - Find “HR-Monitor-ESP32”
- Connect to device
- Find Heart Rate Service (0x180D)
- Enable notifications on Heart Rate Measurement characteristic
- Watch real-time heart rate updates
Learning Outcomes:
- Implement standard BLE GATT services
- Handle BLE server callbacks (connect/disconnect)
- Use BLE notifications for real-time data
- Work with standard Bluetooth SIG services
- Test BLE devices with professional tools
Challenges:
- Add Battery Service (0x180F) with battery level notifications
- Implement actual MAX30102 heart rate sensor reading
- Add energy expended calculation (per BLE HRS specification)
- Implement RR-Interval measurements for heart rate variability
23.4 Lab 2: Python BLE Environmental Monitor Dashboard
Objective: Create Python dashboard that connects to BLE environmental sensors and displays data in real-time.
Materials:
- Python 3.7+
- ESP32 with BLE (from Lab 1 or separate sensor)
- bleak library (
pip install bleak)
Expected Output:
============================================================
BLE Environmental Monitor Dashboard
============================================================
Scanning for devices matching 'HR-Monitor'...
Found: HR-Monitor-ESP32 (A4:CF:12:34:56:78)
Connecting to HR-Monitor-ESP32...
Connected to HR-Monitor-ESP32
Subscribed to Heart Rate notifications
Warning: Battery service not available
Monitoring for 30 seconds...
[14:23:10] Heart Rate: 72 BPM
[14:23:11] Heart Rate: 78 BPM
[14:23:12] Heart Rate: 65 BPM
[14:23:13] Heart Rate: 82 BPM
...
Disconnected
Learning Outcomes:
- Use Python bleak library for BLE communication
- Connect to BLE GATT servers
- Subscribe to BLE notifications
- Parse standard BLE data formats
- Handle asynchronous BLE operations
23.5 Lab 3: BLE Beacon-Based Indoor Positioning
Objective: Use multiple BLE beacons to estimate indoor position using RSSI trilateration.
Materials:
- 3+ ESP32 boards (as iBeacon transmitters)
- 1 ESP32 or Python device (as scanner/receiver)
- Known beacon positions
Beacon Setup (ESP32 #1, #2, #3):
#include <BLEDevice.h>
#include <BLEBeacon.h>
#define BEACON_UUID "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
#define BEACON_MAJOR 1
#define BEACON_MINOR 101 // Change to 102, 103 for other beacons
void setup() {
Serial.begin(115200);
BLEDevice::init("iBeacon");
BLEDevice::createServer();
BLEBeacon beacon;
beacon.setManufacturerId(0x4C00); // Apple's company ID
beacon.setProximityUUID(BLEUUID(BEACON_UUID));
beacon.setMajor(BEACON_MAJOR);
beacon.setMinor(BEACON_MINOR);
beacon.setSignalPower(-59); // Calibrated RSSI at 1 m
BLEAdvertisementData advData;
advData.setFlags(0x04); // BR_EDR_NOT_SUPPORTED
advData.setManufacturerData(beacon.getData());
BLEAdvertising* pAdv = BLEDevice::getAdvertising();
pAdv->setAdvertisementData(advData);
pAdv->start();
}
void loop() { delay(1000); }Expected Output (Python Positioning System):
Indoor Positioning System
============================================================
Known Beacon Positions:
Beacon 101: (0.0m, 0.0m)
Beacon 102: (5.0m, 0.0m)
Beacon 103: (0.0m, 5.0m)
Scanning for beacons...
Scan #1:
Beacon 101: RSSI -52 dBm -> 1.78m
Beacon 102: RSSI -65 dBm -> 4.47m
Beacon 103: RSSI -58 dBm -> 2.82m
Estimated Position: (1.2m, 1.5m)
Scan #2:
Beacon 101: RSSI -55 dBm -> 2.24m
Beacon 102: RSSI -62 dBm -> 3.55m
Beacon 103: RSSI -60 dBm -> 3.16m
Estimated Position: (1.4m, 1.7m)
...
Learning Outcomes:
- Implement iBeacon protocol on ESP32
- Parse BLE beacon advertisement packets
- Convert RSSI to distance using path loss model
- Implement 2D trilateration algorithm
- Build indoor positioning systems
- Understand RSSI limitations and filtering
Challenges:
- Add Kalman filtering for smoother position estimates
- Calibrate path loss exponent (n) for your environment
- Add 4th beacon for 3D positioning
- Implement zone-based proximity instead of exact coordinates
- Create visual map showing beacons and estimated position
23.6 Lab 4: BLE Mesh Network Simulation
Objective: Simulate a BLE mesh network with multiple nodes relaying messages.
Materials:
- 3+ ESP32 boards
- ESP-IDF with BLE Mesh support (or use simulation library)
Example Output (Mesh Simulator):
BLE Mesh Network Simulator
============================================================
Network Topology:
Node1 (switch): Neighbors -> Node2
Node2 (relay): Neighbors -> Node1, Node4
Node3 (light): Neighbors -> Node2, Node5
Node4 (sensor): Neighbors -> Node2, Node5
Node5 (light): Neighbors -> Node3, Node4
============================================================
Test 1: Switch broadcasts LIGHT_SET command
============================================================
Node3 (Light): ON
Node5 (Light): ON
Message reached via 4 paths:
Node1 -> Node2 -> Node3
Node1 -> Node2 -> Node4 -> Node5
Node1 -> Node2 -> Node4
Node1 -> Node2
============================================================
Test 2: Sensor sends data to relay
============================================================
Node2 received sensor data: {'temperature': 22.5, 'humidity': 45}
Message reached via 1 path(s):
Node4 -> Node2
============================================================
Final Node States:
============================================================
Node3 (light): {'on': True}
Node5 (light): {'on': True}
Learning Outcomes:
- Understand BLE mesh network topology
- Implement message flooding with TTL
- Prevent routing loops with message caching
- Model multi-hop communication
- Simulate real-world mesh scenarios
Challenges:
- Implement managed flooding (more efficient than flooding)
- Add friend/low-power node relationships
- Implement publish/subscribe model
- Add network provisioning and key distribution
- Simulate node failures and self-healing
23.7 Worked Examples
Scenario: A smart lock manufacturer needs to implement over-the-air (OTA) firmware updates via BLE. The firmware image is 256 KB and must transfer in under 10 minutes to avoid user frustration. The target device uses Nordic nRF52840 with BLE 5.0 support.
Given:
- Firmware size: 256 KB (262,144 bytes)
- Target transfer time: < 10 minutes (600 seconds)
- Default ATT MTU: 23 bytes (20 bytes usable payload)
- Maximum supported MTU: 247 bytes (244 bytes usable payload)
- Connection interval: 15 ms (configurable)
- Data Length Extension (DLE): supported (251 byte PDU)
- BLE 5.0 2M PHY: supported
Steps:
Calculate minimum required throughput:
- Data to transfer: 262,144 bytes
- Time available: 600 seconds
- Minimum throughput: 262,144 / 600 = 437 bytes/second (approximately 3.5 kbps)
Calculate throughput with default settings (no optimization):
- MTU: 23 bytes - 20 bytes payload per ATT packet
- Connection interval: 15 ms - 66 connection events/second
- Assuming 1 packet per connection event: 20 x 66 = 1,320 bytes/second
- Transfer time: 262,144 / 1,320 = 199 seconds (approximately 3.3 minutes)
- This meets the requirement, but let’s optimize further for better UX
Optimize with MTU exchange:
// After connection established, request MTU exchange void on_connected(uint16_t conn_handle) { // Request 247-byte MTU (maximum for BLE 4.2+) sd_ble_gattc_exchange_mtu_request(conn_handle, 247); } void on_mtu_exchanged(uint16_t conn_handle, uint16_t mtu) { // Negotiated MTU (minimum of both sides) // Usable payload = MTU - 3 (ATT header) g_max_payload = mtu - 3; // 244 bytes with 247 MTU }Calculate optimized throughput:
- MTU: 247 bytes - 244 bytes payload
- With DLE enabled: can send 244 bytes in single LL packet
- Packets per connection event: up to 6 (with 15ms CI)
- Conservative estimate (4 packets/event): 244 x 4 x 66 = 64,416 bytes/second
- Transfer time: 262,144 / 64,416 = 4.1 seconds
Enable 2M PHY for additional speed:
// Request PHY update to 2M after connection ble_gap_phys_t phys = { .tx_phys = BLE_GAP_PHY_2MBPS, .rx_phys = BLE_GAP_PHY_2MBPS }; sd_ble_gap_phy_update(conn_handle, &phys);- 2M PHY doubles bit rate: ~128 kB/second theoretical
- Transfer time: 262,144 / 128,000 = 2 seconds (theoretical maximum)
Result: With MTU 247, DLE, 2M PHY, and Write Without Response, the 256 KB firmware transfers in approximately 5-8 seconds in practice (accounting for protocol overhead and flow control). This represents a 25-40x improvement over default settings.
Key Insight: BLE throughput optimization requires enabling multiple features together: MTU exchange (12x payload increase), Data Length Extension (fewer LL packets), 2M PHY (2x bit rate), and Write Without Response (eliminates ACK latency). Each feature independently provides improvement, but they multiply when combined.
Scenario: An agricultural sensor node monitors soil moisture, temperature, and light levels every 15 minutes and transmits data to a gateway. The device must operate for 2 years on 2x AA batteries without maintenance. The node uses an ESP32-C3 module.
Given:
- Battery capacity: 2x AA (2,800 mAh @ 1.5V = 4,200 mWh at 3V effective after boost converter, 85% efficiency)
- ESP32-C3 deep sleep current: 5 microA
- ESP32-C3 active current: 80 mA (Wi-Fi/BLE radio)
- Soil moisture sensor: 15 mA for 50ms per reading
- Temperature sensor (I2C): 0.5 mA for 10ms
- Light sensor: 0.2 mA for 5ms
- BLE advertising TX: 12 mA for 3ms (connectable advertising)
- BLE connection event: 15 mA for 5ms (TX + RX)
- Report interval: 15 minutes
Steps:
Calculate energy per measurement cycle:
Sensor readings:
- Wake from deep sleep: 80 mA x 2ms = 0.16 mAms
- Soil moisture: 15 mA x 50ms = 0.75 mAms
- Temperature: 0.5 mA x 10ms = 0.005 mAms
- Light: 0.2 mA x 5ms = 0.001 mAms
- Sensor subtotal: 0.92 mAms
BLE transmission (advertising + connection):
- Start advertising: 12 mA x 3ms x 10 events = 0.36 mAms
- Connection event (data TX): 15 mA x 5ms x 3 events = 0.225 mAms
- BLE subtotal: 0.59 mAms
MCU processing:
- Data processing: 30 mA x 5ms = 0.15 mAms
- Total per cycle: 0.92 + 0.59 + 0.15 = 1.66 mAms = 0.00166 mAh
Calculate daily energy consumption:
- Cycles per day: 24h x 4/hour = 96 cycles
- Active energy: 96 x 0.00166 mAh = 0.159 mAh/day
- Deep sleep energy: 5 microA x 24h = 0.12 mAh/day
- Total daily: 0.159 + 0.12 = 0.28 mAh/day
Calculate battery life:
- Usable capacity (80% of rated): 2,800 x 0.8 = 2,240 mAh
- Battery life: 2,240 / 0.28 = 8,000 days = 21.9 years
Reality check - identify hidden consumers:
- Voltage regulator quiescent: ~10 microA - adds 0.24 mAh/day
- RTC crystal oscillator: ~1 microA - adds 0.024 mAh/day
- Leakage currents: ~2 microA - adds 0.048 mAh/day
- Self-discharge (2%/year for alkaline): ~4.7 mAh/month = 0.15 mAh/day
- Revised daily: 0.28 + 0.24 + 0.024 + 0.048 + 0.15 = 0.74 mAh/day
Revised battery life calculation:
- Battery life: 2,240 / 0.74 = 3,027 days = 8.3 years
- Still exceeds 2-year requirement with large margin
Add margin for real-world degradation:
- Temperature effects (cold reduces capacity 20%): 0.8x factor
- Battery aging (10% capacity loss/year): ~0.85x over 2 years
- Connection failures (retry overhead): 1.2x energy estimate
- Worst-case daily: 0.74 x 1.2 = 0.89 mAh/day
- Worst-case capacity: 2,240 x 0.8 x 0.85 = 1,523 mAh
- Conservative estimate: 1,523 / 0.89 = 1,711 days = 4.7 years
Result: The design achieves 4.7+ year battery life under worst-case assumptions, comfortably exceeding the 2-year requirement. Key design decisions: 15-minute report interval (not continuous), deep sleep between readings, short BLE connection using bonding (no re-pairing), and efficient sensor duty-cycling.
Key Insight: Sleep current dominates long-term battery life for infrequent reporting sensors. Always measure actual deep sleep current with a microA-capable meter - leakage from GPIO configuration, pull-ups, and connected sensors often exceeds datasheet MCU values.
Scenario: A smart thermostat sends temperature updates to a smartphone app. The design team debates whether to use a 7.5ms connection interval (fast response) or 100ms interval (low power). Calculate the trade-offs.
Given:
- BLE connection event duration: 3ms (includes TX, RX, and turnaround time)
- Peripheral sleep current: 2 microA
- Peripheral active current (during connection event): 15mA @ 3V
- Battery: 1000mAh coin cell
- Update rate: Temperature change every 30 seconds
Steps:
1. Calculate average latency for user-initiated read:
7.5ms interval: Average latency = 7.5ms / 2 = 3.75ms
100ms interval: Average latency = 100ms / 2 = 50ms
(User taps "refresh" - must wait for next connection event on average)
2. Calculate energy per connection event:
Active energy = 15mA * 3V * 3ms = 135 microjoules
Sleep energy = 2 microA * 3V * 97ms (between events) = 0.6 microjoules
Total per event = 135.6 microjoules
3. Calculate daily energy consumption:
7.5ms interval: (1000ms / 7.5) * 24h * 3600s = 11,520,000 events/day
11,520,000 * 135.6 microjoules = 1,562 joules/day
100ms interval: (1000ms / 100) * 24h * 3600s = 864,000 events/day
864,000 * 135.6 microjoules = 117 joules/day
4. Calculate battery life:
Battery capacity = 1000mAh * 3V * 3600s = 10,800 joules
7.5ms interval: 10,800 / 1,562 = 6.9 days
100ms interval: 10,800 / 117 = 92.3 days (about 3 months)
Result: The 7.5ms interval provides 13x faster response but drains battery 13x faster. For a thermostat (infrequent user interaction), the 100ms interval is far superior.
Key Insight: Connection interval is the dominant factor in BLE power consumption. For applications with infrequent updates, using the longest acceptable interval (typically 100-500ms) extends battery life by 10-50x compared to aggressive intervals. Only use fast intervals (<20ms) for latency-critical applications like wireless audio, gaming controllers, or fitness trackers with real-time step counting.
23.8 Summary
This chapter provided four complete BLE project implementations:
- Lab 1 - Heart Rate Monitor: Standard GATT service implementation with ESP32 and nRF Connect testing
- Lab 2 - Environmental Dashboard: Python bleak client for real-time monitoring with notifications
- Lab 3 - Indoor Positioning: iBeacon deployment and trilateration-based location estimation
- Lab 4 - Mesh Network: BLE mesh simulation with message flooding and multi-hop routing
- Worked Examples: MTU optimization for OTA updates and power budget analysis for battery devices
23.8.1 Knowledge Check: GATT Service Standards
23.8.2 Knowledge Check: BLE Indoor Positioning
23.8.3 Knowledge Check: BLE Power Budget
23.9 Knowledge Check
Common Pitfalls
ESP32 OTA firmware update labs require the OTA bootloader partition table (CONFIG_PARTITION_TABLE_TWO_OTA) and a partition large enough for two firmware images. Flashing a standard single-partition firmware and attempting OTA results in a partition write error. Always use ota_2.csv partition table and verify partition layout with esptool.py read_flash before starting OTA labs.
Arduino BLE libraries (ESP32 BLE Arduino, ArduinoBLE) use different internal architectures than native ESP-IDF NimBLE/Bluedroid. Attempting to use Arduino BLE functions alongside ESP-IDF BLE calls causes duplicate stack initialization and crashes. Choose one framework for the entire project and do not mix API calls across the Arduino and ESP-IDF BLE layers.
If a GATT characteristic requires authenticated write permission for its CCCD, writing 0x0001 without prior pairing and bonding returns ATT Error 0x05 (Insufficient Authentication). The lab must include pairing steps (bonding via nRF Connect: Pair button) before subscribing. Log the ATT error response in nRF Connect to identify authentication failures rather than assuming the notification subscription failed for other reasons.
BLE power optimization labs with a battery-powered device that has low charge will show artificially high power consumption as the voltage regulator drops below its efficient operating range. Always start power profiling labs with a fully charged or bench-power-supplied device at the nominal supply voltage, and document the supply voltage used to ensure reproducible results.
23.10 What’s Next
| Chapter | Focus | Why Read It |
|---|---|---|
| Zigbee Fundamentals | Zigbee network architecture, coordinator/router/end-device roles | Compare BLE mesh with a protocol purpose-built for large-scale IoT mesh deployments |
| Thread and Matter | IP-based mesh networking and the Matter interoperability standard | Understand how IP connectivity over mesh differs from BLE GATT-based communication |
| Wi-Fi for IoT | IEEE 802.11 variants, power management, and IoT integration patterns | Evaluate when Wi-Fi’s higher throughput justifies its greater power cost versus BLE |
| BLE Security | Pairing modes, key distribution, and attack vectors specific to BLE | Secure the GATT services and beacon deployments built in these labs |
| BLE Performance Optimization | Connection parameter tuning, throughput benchmarking, and PHY selection | Apply systematic measurement to the power and throughput trade-offs explored in the worked examples |
| Indoor Positioning Systems | System-level architecture for production IPS deployments | Extend the Lab 3 trilateration prototype to a production-grade positioning platform |