Chapters

20 BLE Field Debugging and Internals

networking
wireless
bluetooth
ble
esp32
lab

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.

The mathematical gist. With a 100 ms interval, 500 µs transmit burst at 15 mA, and 5 µA sleep current, the radio averages 0.0800 mA and a 225 mAh cell gives about 117 days. A missed shutdown that leaves 8 mA in “sleep” raises the average to 8.04 mA and cuts that estimate to 1.17 days.

Math Bridge · guided foundationsHow can one missing shutdown cut 117 days to 1.17?Let Radio Remi connect duty cycle, average current, charge, energy, and pulse sag.

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

Mistake 1: Forgetting to Enable Notifications

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

Mistake 2: Connection Interval Mismatch

Problem: Battery drains quickly or response is too slow.

Cause: Using default connection interval without optimization.

ApplicationRecommended Interval
Game controller7.5-15 ms
Fitness tracker100-200 ms
Temperature sensor1000-4000 ms

Fix: Request appropriate connection parameters after connecting.

Try It: BLE Connection Interval Power Estimator

See how connection interval affects battery life. Shorter intervals mean faster response but higher power consumption — find the right balance for your application.

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

Mistake 3: MTU Size Assumptions

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
Common Mistake: Using Default MTU Size for Large Sensor Payloads

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.

Try It: BLE MTU Payload Calculator

Build your sensor payload by toggling fields on/off. See if your total fits within the default BLE MTU or if you need to negotiate a larger one.

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.

Check: BLE MTU Negotiation

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:

SymptomLikely CauseSolution
Pairing fails repeatedlyWrong PIN, or a pairing method the device cannot supportRe-enter the PIN carefully, and confirm the configured pairing method matches both devices’ I/O capabilities
Device disconnects randomlyOut of rangeMove closer, check battery, and review the granted connection interval and supervision timeout
BLE device not discoverableNot advertisingPress the pairing button, verify power, and confirm the advertising interval is not so long that a scan window keeps missing it
Connection takes very longToo many nearby devicesClear 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:

ToolPlatformUse Case
nRF ConnectiOS/AndroidBLE scanning, GATT browser
LightBlueiOS/AndroidBLE peripheral simulator
WiresharkDesktopPacket capture (with sniffer)
hcitool/gatttoolLinuxCommand-line BLE tools
Nordic nRF SnifferHardwareBLE packet analysis
Check: BLE and Wi-Fi Coexistence

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.

The BLE evidence path begins with advertising and scan exchange, moves through the connection indication, and reaches connected GATT traffic. The same run records the granted interval, peripheral latency, supervision timeout, MTU, notification rate, and disconnect reason.
Figure 20.1: BLE connection flow from advertising through scan request, scan response, connection indication, connected GATT data exchange, and connection parameter bounds.

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.

ChangeEvidence to retest
Raise peripheral latency to save powerRecalculate the timeout bound and log granted latency, timeout, and disconnect reason.
Shorten connection interval for faster controlRerun current-budget and responsiveness measurements using the granted interval.
Negotiate a larger MTU for batched sensor dataWait for the MTU callback, subtract the 3-byte ATT header, and verify notification chunking.
Move from lab phone to production centralRecord 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 elementImplementation noteWhy it matters
Environmental Sensing serviceUse standard UUID 0x181A and Temperature characteristic 0x2A6E where the data format fits.Standard services make generic BLE apps and gateways easier to integrate.
Battery ServiceAdd Battery Service 0x180F and Battery Level 0x2A19.Gateways can alert on low battery without custom parsing.
Notification supportAdd 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 serviceUse a generated 128-bit UUID for proprietary measurements or diagnostics.Avoid overloading standard services with non-standard payloads.
Data formatPack 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:

ExercisePreserve this implementation habitReview evidence
Python BLE scannerHandle 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 beaconAdvertise 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 monitorUse 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 labSeparate RSSI smoothing, calibration samples, and zone classification from the BLE transport code.Calibration table, known-distance samples, and boundary error notes.
Mesh simulationKeep 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.