20 BLE Field Debugging and Internals
20.1 When the Lab Meets the Field
Follow One Failed Notification End to End
Picture a sensor that pairs on the bench but stops sending updates after it is installed behind a cabinet. A green connection icon does not show where the data stopped.
Bluetooth Low Energy is a short-range radio system designed for small data exchanges and low power; it is shortened to BLE.
Mark one update, then test normal delivery, weak signal, a full message, disconnect, and restart. Keep device identities, BLE role and profile, radio readings, connection state, message length, time, result, and first failed boundary.
This trace covers selected faults, not every phone, radio, or stack. The deeper war stories connect symptoms to captures, counters, timing, power, and repair evidence.
Everything in Building BLE Apps on ESP32 works on the bench. This chapter is about the day it does not: notifications that never arrive, connections that eat the battery, payloads that silently truncate. Each war story ends with the evidence that would have caught it — and the internals section explains what the stack was doing all along.
20.2 When It Breaks in the Field
Most BLE bugs are not random. They come from a mismatch between what the code assumes and what the protocol actually agreed to do. These are the war stories to keep nearby when a demo works once and then fails on a different phone, in a different room, or with a larger payload.
20.2.1 War story: notifications never arrive
Problem: Code connects to BLE device but doesn’t receive updates.
Cause: Notifications require writing to CCCD descriptor.
Wrong:
// Only reads once, no updates
value = characteristic.read();
Correct:
// Enable notifications via CCCD
characteristic.getDescriptor(BLEUUID((uint16_t)0x2902))
->writeValue((uint8_t*)"\x01\x00", 2, true);
// Now notifications will arrive in callback
20.2.2 War story: the connection is too fast, too slow, or too expensive
Problem: Battery drains quickly or response is too slow.
Cause: Using default connection interval without optimization.
| Application | Recommended Interval |
|---|---|
| Game controller | 7.5-15 ms |
| Fitness tracker | 100-200 ms |
| Temperature sensor | 1000-4000 ms |
Fix: Request appropriate connection parameters after connecting.
20.3 Start With the Story
The field failure rarely says “Bluetooth is broken.” It says notifications stopped, the battery collapsed, the MTU was too small, the connection interval drifted, or the lab trace no longer matches the installed device.
Use this chapter as a debugging trail. Reproduce one symptom, capture the radio and application evidence, change one variable, and keep the smallest explanation that makes the deployed behavior understandable.
20.3.1 War story: the payload grows past the default MTU
Problem: Large data packets get truncated.
Cause: Default MTU is only 23 bytes (20 payload).
Fix: Negotiate larger MTU after connection:
// Request MTU exchange
BLEDevice::setMTU(247); // Request 247 bytes
// Actual MTU may be less based on negotiation
The Mistake: Sending sensor data packets larger than 20 bytes without negotiating a larger MTU (Maximum Transmission Unit), causing data truncation, protocol errors, or silent packet loss. This often manifests as “missing bytes” or “corrupted readings” that work fine in testing with short payloads but fail in production with full data.
Why It Happens: The default BLE MTU is only 23 bytes (20 bytes usable payload after 3-byte ATT header overhead). Developers test with simple sensor values (2-4 bytes) that fit easily, then add more features (timestamps, multiple readings, metadata) pushing total payload to 30-50 bytes without realizing MTU negotiation is required.
Real-World Impact:
Scenario: Environmental sensor sends combined reading:
- Temperature: 2 bytes (int16)
- Humidity: 2 bytes (uint16)
- Pressure: 4 bytes (uint32)
- Timestamp: 4 bytes (uint32_t)
- Battery: 1 byte (uint8)
- Device ID: 6 bytes (MAC address)
Total: 19 bytes ✓ (fits in default 20-byte MTU)
Then you add:
- CO2 level: 2 bytes
- Light intensity: 2 bytes
Total: 23 bytes ✗ (exceeds 20-byte payload!)
Result without MTU negotiation:
- First 20 bytes transmitted
- Last 3 bytes silently dropped
- Light intensity always reads 0
- Hours of debugging "sensor malfunction"
The Fix: Always negotiate MTU after connection establishment:
// ESP32 Example: Request larger MTU
void on_connected(uint16_t conn_handle) {
// Request 247 bytes (maximum for BLE 4.2+)
esp_ble_gattc_send_mtu_req(conn_handle, 247);
// Wait for MTU exchange callback before sending data!
mtu_negotiated = false;
}
void on_mtu_exchanged(uint16_t conn_handle, uint16_t mtu) {
Serial.printf("MTU negotiated: %d bytes\n", mtu);
effective_mtu = mtu;
max_payload = mtu - 3; // Subtract ATT header
mtu_negotiated = true;
Serial.printf("Max payload: %d bytes\n", max_payload);
// NOW safe to send large packets
}
// Only send when MTU is ready
void send_sensor_data() {
if (!mtu_negotiated) {
Serial.println("ERROR: Attempted to send before MTU negotiation!");
return;
}
if (payload_size > max_payload) {
Serial.printf("ERROR: Payload %d exceeds MTU %d\n",
payload_size, max_payload);
return;
}
pCharacteristic->setValue(data, payload_size);
pCharacteristic->notify();
}
Alternative: Chunking Strategy (if MTU negotiation fails):
// Fallback for devices that won't negotiate larger MTU
void send_large_data_chunked(uint8_t* data, size_t total_len) {
const size_t CHUNK_SIZE = max_payload - 2; // Reserve 2 bytes for sequence
for (size_t offset = 0; offset < total_len; offset += CHUNK_SIZE) {
size_t chunk_len = min(CHUNK_SIZE, total_len - offset);
// Packet format: [sequence_number] [chunk_data]
uint8_t packet[max_payload];
packet[0] = offset / CHUNK_SIZE; // Chunk sequence
packet[1] = (offset + chunk_len >= total_len) ? 1 : 0; // Last chunk flag
memcpy(&packet[2], data + offset, chunk_len);
pCharacteristic->setValue(packet, chunk_len + 2);
pCharacteristic->notify();
delay(20); // Allow time for transmission
}
}
Key Insight: The 23-byte default MTU is a BLE legacy constraint. Always negotiate MTU to 247 bytes (BLE 4.2+) immediately after connection. If you forget, your application will work fine until you exceed 20 bytes, then fail mysteriously.
Common Pitfalls
Four traps that account for most avoidable BLE field failures. Each panel below names the trap — expand it for why it bites and the concrete fix.
ESP32’s Bluedroid BLE stack requires ~120 kB RAM; NimBLE requires only ~40 kB. For IoT sensor projects that only need BLE (no Classic Bluetooth), using Bluedroid wastes 80 kB of RAM that could be used for application buffers. Select NimBLE via menuconfig (CONFIG_BT_NIMBLE_ENABLED=y) unless Classic Bluetooth profiles (A2DP, HFP) are required.
BLE event handlers (NimBLE ble_hs_cfg.sync_cb, gap_event_cb) run in the NimBLE host task context. Calling blocking operations (vTaskDelay, I2C sensor reads) inside these handlers blocks all BLE protocol processing, causing connection timeouts. Dispatch application work to a separate FreeRTOS task using xQueueSend() and return immediately from BLE callbacks.
Entering ESP32 deep sleep without calling ble_hs_stop(), nimble_port_stop(), nimble_port_deinit(), and esp_bt_controller_disable() causes the BLE controller to consume ~8 mA during sleep instead of ~10 µA. Always perform a clean BLE shutdown sequence before calling esp_deep_sleep_start() and re-initialize the stack upon wakeup if connections are needed.
Setting BLE_SM_IO_CAP_NO_INPUT_NO_OUTPUT (Just Works pairing) for a device that stores sensitive user data provides zero MITM protection. Just Works pairing generates an unauthenticated LTK that any BLE central can establish without user confirmation. For devices handling health, financial, or access-control data, require at minimum Passkey Entry (IO_CAP_DISP_ONLY or IO_CAP_KEYBOARD_ONLY) with MITM protection flag.
20.3.2 Troubleshooting Quick Reference
Not every field failure is a notification, interval, or MTU bug. Before reaching for a sniffer, walk the connection lifecycle from the outside: can the device be found, does pairing complete, does the link hold, and does application data move once it is connected.
Connection lifecycle checklist:
Work through Troubleshooting Quick Reference as a connected sequence. Start with Can the device be discovered? Check advertising state, pairing-mode timeout, power, and any scan filters. Next, Does pairing fail? Confirm the pairing method matches both devices’ I/O capabilities and remove stale bonds before retrying — if the failure looks like a method-selection problem rather than a one-off retry, see BLE Pairing Methods. Then, Does it connect but drop? Check range, battery level, the granted connection interval, and Wi-Fi interference (see the coexistence check below). Finally, Does data fail after connection? Verify service and characteristic UUIDs and characteristic properties first — if the specific symptom is a missing notification stream or a truncated payload, that is Mistake 1 or Mistake 3 above, not a new problem.
Common problems and solutions:
| Symptom | Likely Cause | Solution |
|---|---|---|
| Pairing fails repeatedly | Wrong PIN, or a pairing method the device cannot support | Re-enter the PIN carefully, and confirm the configured pairing method matches both devices’ I/O capabilities |
| Device disconnects randomly | Out of range | Move closer, check battery, and review the granted connection interval and supervision timeout |
| BLE device not discoverable | Not advertising | Press the pairing button, verify power, and confirm the advertising interval is not so long that a scan window keeps missing it |
| Connection takes very long | Too many nearby devices | Clear the paired list and re-scan |
Debug checklist:
Pairing and discovery:
Work through Troubleshooting Quick Reference as a connected sequence. Start with [ ] Verify Bluetooth is enabled on both devices. Next, [ ] Check the device is in pairing mode (usually a 2-3 minute timeout). Then, [ ] Remove old pairings from the device list. Finally, [ ] Confirm Bluetooth version compatibility.
Connection:
Work through Troubleshooting Quick Reference as a connected sequence. Start with [ ] Verify devices are within range (10m for BLE). Next, [ ] Check for physical obstacles. Then, [ ] Identify interference sources (Wi-Fi, microwaves). Finally, [ ] Monitor RSSI (should be above -80 dBm).
Advertising and services:
Work through Troubleshooting Quick Reference as a connected sequence. Start with [ ] Check advertising interval (100 ms-10s typical). Then connect that result to [ ] Review GATT service/characteristic UUIDs.
Debugging tools:
| Tool | Platform | Use Case |
|---|---|---|
| nRF Connect | iOS/Android | BLE scanning, GATT browser |
| LightBlue | iOS/Android | BLE peripheral simulator |
| Wireshark | Desktop | Packet capture (with sniffer) |
| hcitool/gatttool | Linux | Command-line BLE tools |
| Nordic nRF Sniffer | Hardware | BLE packet analysis |
When these failures are under control, the build starts looking like a product rather than a lab. The closing sections keep the same ideas but ask the production questions: power, security, evidence, and what to study next.
20.4 Under the Hood: Parameters, Payloads, and Internals
The war stories above diagnose a working build from the outside. This section is for the reader who wants to know what the stack is really doing — the parameter coupling behind connection timing, and the full lab and code notes behind the build in BLE App Development on ESP32.
20.4.1 Connection Parameter Evidence and Timeout Bounds
The connection-interval mistake above is not only a battery problem; it is a parameter-coupling problem. Inspect Figure 20.1 to place the granted parameters on the same timeline as discovery, connection, and GATT exchange. A demo that connects once does not prove latency, battery, or recovery claims.
Read Figure 20.1 from advertising through scan request and response to the connection indication, then continue into GATT data exchange. At that point, record the granted interval, latency, timeout, MTU, notification rate, and disconnect reason together. The ordered evidence connects radio setup to the timeout bounds calculated next.
Peripheral latency is useful because it lets a device keep a short interval available for central-initiated work while sleeping through idle events. For example, a 50 ms interval with latency 4 gives a worst idle listening gap of about (1 + 4) x 50 ms = 250 ms, before application processing and phone scheduling are added. A 1 s interval with latency 9 gives about (1 + 9) x 1 s = 10 s, which can fit a slow environmental sensor but would feel broken for a lock, controller, or vibration alarm.
The supervision timeout must cover the longest legal silence, not the average report period:
supervision_timeout > (1 + peripheral_latency) x connection_interval x 2
With interval 30 ms and latency 4, the bound is (1 + 4) x 30 ms x 2 = 300 ms, so a 2 s supervision timeout is valid and a 200 ms timeout is not. With interval 1 s and latency 9, the bound is 20 s; a 10 s timeout detects loss sooner, but it is invalid for that latency. This is the hidden reason slow sensors often need long supervision timeouts when they also use high latency.
| Change | Evidence to retest |
|---|---|
| Raise peripheral latency to save power | Recalculate the timeout bound and log granted latency, timeout, and disconnect reason. |
| Shorten connection interval for faster control | Rerun current-budget and responsiveness measurements using the granted interval. |
| Negotiate a larger MTU for batched sensor data | Wait for the MTU callback, subtract the 3-byte ATT header, and verify notification chunking. |
| Move from lab phone to production central | Record the central-granted parameters, because mobile and gateway hosts may reject requested values. |
20.4.2 Folded Lab Notes: Sensor Beacon and Deployment Tuning
Use the existing GATT server lab as the foundation for a sensor beacon extension:
| Lab element | Implementation note | Why it matters |
|---|---|---|
| Environmental Sensing service | Use standard UUID 0x181A and Temperature characteristic 0x2A6E where the data format fits. | Standard services make generic BLE apps and gateways easier to integrate. |
| Battery Service | Add Battery Service 0x180F and Battery Level 0x2A19. | Gateways can alert on low battery without custom parsing. |
| Notification support | Add a Client Characteristic Configuration Descriptor, such as BLE2902 on ESP32 Arduino examples. | The NOTIFY property alone is not enough; clients must be able to enable notifications. |
| Custom service | Use a generated 128-bit UUID for proprietary measurements or diagnostics. | Avoid overloading standard services with non-standard payloads. |
| Data format | Pack multi-byte values in the expected byte order and document scale factors. | Many BLE bugs look like bad sensors but are actually byte order or resolution mistakes. |
When the lab moves toward production, tune the radio behavior in three areas.
Payload discipline. Do not send large payloads immediately after requesting a larger MTU; wait for the negotiated MTU event, then subtract the 3-byte ATT header before sizing each write or notification.
Timing choices. Avoid continuous scanning on battery-powered gateways: match scan window and interval to the beacon advertising interval and the discovery latency you can accept. Pick the connection interval from the application latency requirement, then apply peripheral latency for routine sleep — a slow environmental sensor tolerates longer intervals than a lock, controller, or vibration monitor.
Field calibration. Calibrate RSSI in the deployment environment before using it for indoor positioning; a short walk test at known distances usually beats datasheet assumptions. For zone detection, model beacon placement around zone boundaries, not only zone centers — boundary ambiguity often dominates classification error.
20.4.3 Folded Code and Lab Portfolio Notes
Use the scanner, beacon, dashboard, and mesh exercises as a portfolio rather than as isolated demos:
| Exercise | Preserve this implementation habit | Review evidence |
|---|---|---|
| Python BLE scanner | Handle scan exceptions, disconnects, duplicate advertisements, and adapter permission failures. Avoid synchronous BLE calls inside an event loop. | Error log, retry behavior, and a filtered device list from a real scan. |
| Arduino or ESP32 beacon | Advertise a clear service UUID, keep the interval realistic, and record the transmit power used for distance estimates. | Advertising packet capture, interval setting, and current draw sample. |
| Heart-rate or environmental monitor | Use standard services where possible and require the client to enable the CCCD before expecting updates. | Service discovery output and notification callback proof. |
| Dashboard or positioning lab | Separate RSSI smoothing, calibration samples, and zone classification from the BLE transport code. | Calibration table, known-distance samples, and boundary error notes. |
| Mesh simulation | Keep relay, friend, and low-power roles explicit so the design does not assume every node can forward traffic. | Node-role map and message path trace. |
For throughput and power worked examples, state the negotiated MTU, connection interval, peripheral latency, notification rate, and current budget together. Those parameters interact: a larger MTU can shorten an OTA transfer, but a short connection interval or continuous scanning can erase the battery benefit.
Keep Building
Debugging sharpened the build - now put it to work: Indoor Positioning with BLE Beacons turns your beacons into an indoor GPS.
