How to Debug BLE Connection Failures with a Sniffer

Electronics engineer using Bluetooth Low Energy sniffer to analyze wireless connection problems on laptop screen

Your firmware log says DISCONNECTED - REASON: REMOTE USER TERMINATED CONNECTION. Your central’s log says CONNECTION TIMEOUT. Neither one is lying, but neither one is telling you anything useful either. You’re staring at two devices that refuse to connect, each blaming the other, and no amount of printf debugging will show you what’s actually happening in the 2.4 GHz air between them.

This is the fundamental problem: application-layer logs report outcomes, not causes. The actual negotiation, every CONNECT_IND, every rejected parameter, every failed key exchange, happens over the air at the link layer, and your firmware only sees the aftermath. A sniffer shows you the conversation itself.

This article is a systematic, phase-by-phase methodology for using an nRF Sniffer with Wireshark to diagnose BLE connection failures. We’re skipping setup entirely. If you need to flash your dongle and install the Wireshark plugin, [follow Nordic’s setup guide] first. Here, we’re focused on what to do once you’re capturing packets, and how to pinpoint exactly where and why a connection breaks.

The Five Phases Where Connections Die

Before opening Wireshark, you need a diagnostic map. BLE connections don’t just “fail.” They fail at a specific phase, and each phase has distinct packet signatures. Think of this as your decision tree:

  1. Advertising — The peripheral isn’t visible, or it’s advertising in a way the central can’t connect to.
  2. Initiation — The central sends CONNECT_IND, but the peripheral misses it or the link never bootstraps.
  3. Parameter Negotiation — Connection technically forms, but the two sides can’t agree on connection intervals, latency, or timeout.
  4. Pairing & Encryption — SMP exchange fails due to key mismatch, capability mismatch, or stale bonding data.
  5. Service Discovery — GATT discovery times out, MTU negotiation breaks, or permission errors block attribute reads.

Here’s the critical insight: many “connection failures” are actually successful connections that fail during phase 3, 4, or 5. If you’re only looking at advertising and initiation, you’ll miss the real problem entirely. Identify the failing phase first. Everything else follows.

Configuring Your Capture for Connection Debugging

This isn’t about installation. It’s about capture strategy that saves you time.

Channel selection matters. BLE advertising rotates across channels 37, 38, and 39. The nRF Sniffer can only follow one at a time during the advertising phase. If you don’t see advertising packets, try a different channel before assuming the peripheral is silent. You may need two or three short captures across different channels.

Enable “Follow” mode. In the nRF Sniffer interface, select the target device’s address so the sniffer automatically hops to data channels once CONNECT_IND arrives. Without this, you’ll capture the connection initiation and then lose everything afterward.

Set up your Wireshark columns. Add custom columns for: Packet Type (btle.advertising_header.pdu_type), LL Opcode (btle.link_layer.control_opcode), and Delta Time. Right-click any column header → Column Preferences to add these.

Add coloring rules. Go to View → Coloring Rules and create entries that highlight LL_REJECT_IND, LL_TERMINATE_IND, and SMP Pairing Failed packets in red. When a failure is bright red in a sea of normal traffic, you’ll spot it in seconds instead of scrolling for minutes.

Keep captures short. Trigger the connection attempt, capture for 30 seconds, stop. Long captures bury the signal in noise.

Phase-by-Phase BLE Sniffer Debugging

This is the core methodology. Jump to whichever phase matches your symptoms.

Phase 1: Advertising Failures

Filter: btle.advertising_header

What normal looks like: A steady cadence of ADV_IND (connectable undirected) or ADV_DIRECT_IND (connectable directed) packets at the interval you configured, typically every 100–500 ms.

Failure signatures:

  • No advertising packets at all. You’re either on the wrong channel, the peripheral’s firmware hasn’t entered the advertising state, or there’s an RF issue (antenna disconnected, shielding). Try all three advertising channels before panicking.
  • Wrong advertising type. You see ADV_NONCONN_IND or ADV_SCAN_IND instead of ADV_IND. The peripheral is advertising, but not in a connectable mode. The central literally cannot initiate a connection. This is a firmware configuration error; check your advertising type setting.
  • Advertising interval too long. If the peripheral advertises every 10 seconds but the central’s scan window is 2 seconds with a 5-second scan interval, they may rarely overlap. Filter and look at delta times between advertising packets. Apple recommends ≤ 152.5 ms for fast initial connection.

Fix direction: Check your advertising start call, advertising type enum, and interval parameters in firmware.

Phase 2: Connection Initiation Failures

Filter: btle.link_layer.control_opcode == 0x05 for CONNECT_IND, or more broadly btle.advertising_header.pdu_type == 0x05

What normal looks like: ADV_INDCONNECT_IND (same packet exchange) → first data channel packet arrives within the first connection interval (the connection event).

Failure signatures:

  • CONNECT_IND never appears. The central isn’t initiating. The problem is central-side: the scanning/initiating code hasn’t matched the peripheral’s address or the filter policy is excluding it. The peripheral is doing its job.
  • CONNECT_IND sent, but no data packets follow. This is a timing failure. The peripheral either didn’t receive the CONNECT_IND (RF collision, marginal signal) or couldn’t tune to the data channel in time. You’ll sometimes see the central retry with another CONNECT_IND after a supervision timeout.
  • Repeated CONNECT_IND packets to the same peer. The connection keeps forming and immediately dying. Look at the CONNECT_IND payload and check the window size, window offset, and interval fields. A misconfigured connection interval (e.g., 0) will kill the link instantly.

Fix direction: RF environment (move devices closer, eliminate interference for testing), central-side stack configuration, or window timing parameters.

Phase 3: Parameter Negotiation Failures

Filter: btle.link_layer.control || btl2cap

What normal looks like: After connection, you may see LL_CONNECTION_UPDATE_REQ at the link layer or L2CAP Connection Parameter Update Request at L2CAP level, followed by acceptance and a smoothly running connection at the new parameters.

Failure signatures:

  • LL_REJECT_IND or LL_REJECT_EXT_IND after a parameter request. Check the error code in the packet. 0x3B (Unacceptable Connection Parameters) is the most common. This means the central refused the peripheral’s requested parameters.
  • Connection drops exactly at supervision timeout boundary. The parameters were technically accepted, but the interval/latency combination is unsustainable. Calculate: supervision timeout must be > (1 + connSlaveLatency) × connInterval × 2. If it isn’t, the spec says the connection will be terminated.
  • iOS-specific rejections. Apple enforces strict ranges: interval 15–2000 ms, slave latency ≤ 30, supervision timeout 2–6 seconds, and the timeout must be ≥ connInterval × (slaveLatency + 1) × 3. If your parameters violate these constraints, iOS will reject or disconnect silently.

Fix direction: Adjust your connection parameter request to fall within the central platform’s accepted ranges. Test against both iOS and Android, as their tolerances differ significantly.

Phase 4: Pairing and Encryption Failures

Filter: btsmp

What normal looks like: Pairing RequestPairing ResponsePairing ConfirmPairing RandomLL_ENC_REQLL_ENC_RSPLL_START_ENC_REQLL_START_ENC_RSP. Clean, sequential, done.

Failure signatures:

  • SMP Pairing Failed packet. The reason code tells you exactly what happened:
    • 0x01 Passkey Entry Failed
    • 0x02 OOB Not Available
    • 0x03 Authentication Requirements (IO capabilities don’t support the required pairing method)
    • 0x04 Confirm Value Failed (key derivation mismatch, often a stale bond)
    • 0x05 Pairing Not Supported
    • 0x06 Encryption Key Size (requested key size below minimum)
  • LL_TERMINATE_IND immediately after LL_START_ENC_RSP. This is the classic stale bonding signature. The peripheral encrypted with an old LTK; the central has a different one (or vice versa). Encryption technically “started” but the first encrypted packet couldn’t be decrypted, so the link is torn down.
  • LL_ENC_REQ sent, no LL_ENC_RSP. One side doesn’t support the required encryption level or the SMP state machine is stuck.

The #1 cause of “it worked yesterday” failures: stale bonding information. One device was factory-reset or had its bond table cleared while the other still has the old keys. In the capture, you’ll see the encryption start sequence complete, then LL_TERMINATE_IND with reason 0x06 (PIN or Key Missing) or 0x3D (Authentication Failure) within 1–2 connection events. The fix is to delete the bond on both sides and re-pair.

Fix direction: Ensure bond storage is persistent across resets, implement bond-clearing mechanisms accessible to the user, and match IO capabilities between devices.

Phase 5: Service Discovery and GATT Failures

Filter: btatt.opcode

What normal looks like: Read By Group Type Request (discovering services) → ResponseRead By Type Request (discovering characteristics) → ResponseFind Information Request (discovering descriptors) → Response. Clean request/response flow.

Failure signatures:

  • ATT Error Response packets. Check the error code: 0x05 (Insufficient Authentication) means the client needs to encrypt first but hasn’t. 0x0A (Attribute Not Found) means the GATT database doesn’t contain what the central is looking for.
  • Discovery starts but never completes. The peripheral is responding too slowly, and the central’s discovery timeout fires first. Look at delta times between request and response. If responses take > 30 seconds, the central may give up. Also check MTU: if MTU negotiation failed silently, large attribute tables may require far more round-trips than expected.
  • Zero GATT traffic post-connection. The connection is alive at the link layer, but no ATT packets appear. This usually means encryption is required before GATT access (permission flags on characteristics), but the central never initiated pairing.

Fix direction: Review GATT characteristic permission flags, ensure encryption happens before GATT access when required, and test MTU negotiation with btatt.opcode == 0x02 (Exchange MTU Request).

Tracking Down Intermittent Failures

Intermittent failures are the most expensive to debug, but the sniffer gives you an advantage.

Use delta-time analysis. Sort by the delta-time column. In a healthy connection, consecutive data packets arrive at regular intervals (your connection interval ± jitter). If you see a gap that exceeds the supervision timeout, that’s your disconnection point. Work backward from there.

Filter for termination reasons. btle.link_layer.control_opcode == 0x02 catches LL_TERMINATE_IND. The reason code payload tells you the cause: 0x08 is connection timeout (the other side went silent), 0x13 is remote user terminated (intentional disconnect), 0x3D is authentication failure.

Examine channel maps. Filter for LL_CHANNEL_MAP_IND. If a device is using a channel map that overlaps heavily with your local Wi-Fi (channels 1, 6, or 11 in the 2.4 GHz band), data packets will collide. A shrinking channel map over time suggests the adaptive frequency hopping is struggling with your RF environment.

Capture multiple failures. One capture is anecdotal. Five captures showing the same failure phase is a pattern. If failures are randomly distributed across all five phases, suspect the RF environment, not firmware.

Quick Reference: Filters and Error Codes

Essential Wireshark Display Filters:

PurposeFilter
All advertising packetsbtle.advertising_header
Connection initiationbtle.advertising_header.pdu_type == 0x05
Link layer controlbtle.link_layer.control
Reject/terminate onlybtle.link_layer.control_opcode == 0x02 || btle.link_layer.control_opcode == 0x11
SMP (pairing)btsmp
ATT/GATTbtatt
ATT errors onlybtatt.opcode == 0x01

Common LL_TERMINATE_IND Reason Codes:

CodeMeaning
0x05Authentication Failure
0x06PIN or Key Missing
0x08Connection Timeout
0x13Remote User Terminated Connection
0x16Instant Passed
0x22LMP Response Timeout
0x3BUnacceptable Connection Parameters
0x3DAuthentication Failure (MIC)

Bookmark this section. You’ll be back.

Making Sniffer Debugging Your Default Second Step

Stop treating the sniffer as a last resort. The workflow should be: check logs, then capture packets. Not: check logs, re-read the spec, change random parameters, re-flash, repeat for three days, then reluctantly open Wireshark.

One habit that pays for itself immediately: capture a working connection and save it. Label it baseline_good_connection.pcapng. When something breaks, you have a packet-level reference to diff against. The failing capture will diverge from the baseline at exactly one phase, and that’s where you look.


Hubble Network connects your BLE devices directly to satellite — no gateways, no sniffers, no debugging range limitations on the ground. See how it works →