How to Capture BLE Packets with nRF Sniffer and Wireshark

Electronics workbench with nRF development board connected to laptop running Wireshark packet analysis software

Your BLE device fails to connect to a specific phone model, but only in the field. Your logs say the connection was established. The phone’s Bluetooth settings say “Connected.” Yet the GATT service discovery never completes, and the user sees a spinning wheel until it times out. Your application-layer logs are useless here—they show what your firmware thinks happened, not what actually went over the air. While smartphone BLE scanner apps can help with initial triage, the bug lives in the gap between those two realities.

Over-the-air packet capture closes that gap. It shows you the actual PDUs exchanged between radios: timing, parameters, error codes, and the exact byte where a negotiation went sideways. It’s the difference between reading a call log and listening to the conversation. For readers new to BLE terminology, start with our BLE basics guide to understand the fundamental concepts. For interoperability issues, connection parameter disputes, and subtle GATT-level failures, there’s no substitute.

This article walks you through setting up the nRF Sniffer for Bluetooth LE (v4.1.x) with an nRF52840 DK and Wireshark 4.x, then teaches you how to interpret what you capture. By the end, you’ll have a working sniffer, a library of BLE-specific Wireshark filters, and the skills to diagnose real firmware issues from a pcap file.

Hardware and Software You’ll Need

Hardware: An nRF52840 DK (PCA10056) is the recommended board. The nRF52840 Dongle (PCA10059) also works but lacks an onboard debugger, making firmware flashing less convenient (you’ll need nRF Connect Desktop’s DFU process instead of direct programming). Either board supports all BLE PHYs: 1M, 2M, and Coded.

Software:

  • Wireshark 4.x (4.0 or later; older 3.x versions work but have inferior BLE dissector support)
  • nRF Sniffer for Bluetooth LE v4.1.x — download from Nordic’s nRF Sniffer for Bluetooth LE page on the Infocenter or the GitHub releases
  • Python 3.7+ with pyserial installed (pip install pyserial)
  • nRF Connect for Desktop with the Programmer app (for flashing), or nrfjprog CLI if you prefer

All three host OSes are supported: Windows, macOS, and Linux. Linux users should be aware of serial port permission requirements (you’ll likely need to add your user to the dialout group).

Flashing the nRF Sniffer Firmware

The sniffer firmware turns your DK into a passive BLE radio receiver. It ships as a pre-built hex file inside the downloaded sniffer package.

  1. Extract the nRF Sniffer download. Locate the hex file at hex/sniffer_nrf52840dk_nrf52840_x.x.x.hex (the exact filename reflects the version).
  2. Connect your nRF52840 DK via USB. Confirm it enumerates as a J-Link device.
  3. Open nRF Connect for Desktop → Programmer. Select your DK from the device dropdown.
  4. Click Add file, navigate to the sniffer hex file, and load it.
  5. Click Erase & Write. This performs a full chip erase before programming, which is critical if you’ve previously flashed a SoftDevice or nRF Connect SDK application.
  6. After flashing completes, the DK resets automatically.

CLI alternative: If you prefer nrfjprog:

nrfjprog --eraseall -f nrf52
nrfjprog --program sniffer_nrf52840dk_nrf52840_x.x.x.hex --verify -f nrf52
nrfjprog --reset -f nrf52

Verification: After flashing, the DK should enumerate a new COM/serial port (distinct from the J-Link CDC port). On Windows, check Device Manager for a new USB serial device. On macOS/Linux, look for /dev/tty.usbmodem* or /dev/ttyACM*.

Common pitfall: If you skip the full erase, leftover SoftDevice or application code can conflict with the sniffer firmware. Symptoms include the board not enumerating a serial port, or Wireshark failing to communicate with the interface. Always do a full chip erase first.

Installing the Wireshark Extcap Plugin

The nRF Sniffer communicates with Wireshark through an extcap interface, a Python script that bridges the serial port to Wireshark’s capture engine. Getting the files into the right directory is the critical step.

  1. Inside the extracted sniffer package, find the extcap folder. It contains nrf_sniffer_ble.py, a SnifferAPI subfolder, and supporting files.
  2. Locate your Wireshark extcap directory:
    • Windows: C:\Users\<you>\AppData\Roaming\Wireshark\extcap\
    • macOS: /Applications/Wireshark.app/Contents/MacOS/extcap/ (or the personal path shown in Wireshark → About → Folders)
    • Linux: ~/.config/wireshark/extcap/ or /usr/lib/x86_64-linux-gnu/wireshark/extcap/
  3. Copy the entire contents of the sniffer’s extcap folder into Wireshark’s extcap directory. Don’t nest it inside a subfolder. nrf_sniffer_ble.py must sit directly in the extcap path.
  4. On macOS/Linux, make the script executable: chmod +x nrf_sniffer_ble.py.
  5. Ensure the Python pyserial package is installed for the Python interpreter that nrf_sniffer_ble.py will use. Test with: python3 -c "import serial; print(serial.VERSION)".

Verify installation: Restart Wireshark. Go to Help → About Wireshark → Plugins (or just check the main interface list). You should see “nRF Sniffer for Bluetooth LE” listed as a capture interface with a serial port identifier.

Also import the provided Wireshark profile: Edit → Configuration Profiles → Import, then select the Profile_nRF_Sniffer_Bluetooth_LE folder from the sniffer package. This gives you BLE-optimized coloring rules, column layout, and display filters out of the box.

Troubleshooting: The most common failure is Wireshark not finding the interface. Causes: wrong Python version on PATH (the script needs the Python where pyserial is installed), the script not being executable (Linux/macOS), or files placed in a nested subfolder. Run nrf_sniffer_ble.py --extcap-interfaces from the command line to test independently.

Capturing Your First BLE Packets

Select the nRF Sniffer for Bluetooth LE interface in Wireshark’s main screen. Before starting the capture, click the gear icon to configure capture options:

  • PHY selection: Default is 1M PHY, which captures standard advertising. Switch to 2M or Coded if your target devices negotiate those PHYs post-connection. For initial debugging, 1M is almost always the right starting point.
  • Advertising channel filter: You can limit capture to channels 37, 38, or 39 individually, but leave all three enabled unless you’re investigating channel-specific interference.

Click Start. You’ll immediately see a flood of advertising PDUs from every BLE device within radio range: phones, fitness trackers, beacons, your colleague’s earbuds. This is normal.

Following a specific device: In the Wireshark toolbar (if you loaded the BLE profile), you’ll see a device list dropdown. As the sniffer discovers advertising devices, they populate this list by address and name. Select your target device. The sniffer will then track that device through connection establishment and follow the subsequent data channel communication by synchronizing to the connection’s hop sequence.

This is the key constraint to understand: the nRF Sniffer follows one connection at a time. Once locked to a connection, it hops channels in sync with that link. It can’t simultaneously capture a second connection or advertising traffic from other devices.

Save your captures as .pcapng (Wireshark’s default). This format preserves the BLE metadata, including RSSI, channel index, and PHY, all essential context for debugging.

Reading BLE Packets Like a Firmware Engineer

This is where the nRF Sniffer setup pays off. Capturing packets is trivial. Knowing what they mean is the skill.

Advertising PDU Anatomy

Expand any advertising packet in Wireshark’s dissector pane. You’ll see:

  • Access Address: 0x8E89BED6 for all advertising channel PDUs (per the BLE spec). If you see anything else here, you’re looking at a data channel packet.
  • PDU Header: Contains the PDU type (ADV_IND, ADV_DIRECT_IND, SCAN_REQ, SCAN_RSP, etc.), TxAdd/RxAdd flags (public vs. random address), and payload length.
  • Advertiser Address: The 6-byte device address. Check the Tx Add bit to determine if it’s public or random. This is a common source of connection failures when address types are mismatched.
  • CRC: Wireshark validates this and flags mismatches. CRC errors in advertising suggest RF interference or a marginal radio.

Following a Connection Establishment

The sequence you’re looking for:

  1. ADV_IND (or ADV_DIRECT_IND) from the peripheral
  2. CONNECT_IND from the central. This is the critical PDU. Expand it to inspect:
    • Initiator/Advertiser addresses and their types
    • Access Address assigned to the new connection (used for all subsequent data channel packets)
    • Connection parameters: interval, latency, supervision timeout, channel map, hop increment
  3. First data channel PDU, typically an LL_VERSION_IND or LL_FEATURE_REQ/RSP exchange

The connection parameters in CONNECT_IND tell you the initial negotiated values. If the central requests a 7.5ms interval but your peripheral can’t sustain it, you’ll see an LL_CONNECTION_UPDATE_IND or LL_REJECT_IND later in the trace.

GATT Layer Inspection

Drill deeper into data channel packets to reach the ATT (Attribute Protocol) layer. Wireshark fully dissects ATT operations:

  • ATT Read Request / Response: Shows the handle being read and the returned value. Map handles back to your GATT database to confirm the right characteristic is being accessed.
  • ATT Write Request / Write Command: Distinguish between these. Write Requests expect a response; Write Commands don’t. A central sending Write Commands to a characteristic that requires Write Requests will fail silently from the central’s perspective.
  • ATT Error Response: The most useful packet for debugging. It contains the offending opcode, the attribute handle, and the error code. Common codes: 0x05 (Insufficient Authentication, meaning pairing is required), 0x06 (Request Not Supported), 0x0A (Attribute Not Found), 0x0E (Unlikely Error, often a firmware bug in the GATT server).

Link-Layer Control Procedures

Filter for LL Control PDUs to see the housekeeping that happens beneath GATT:

  • LL_CONNECTION_PARAM_REQ / LL_CONNECTION_PARAM_RSP: One side is requesting new connection parameters. If this is followed by LL_REJECT_EXT_IND, the other side refused, and the reason code tells you why.
  • LL_PHY_REQ / LL_PHY_RSP / LL_PHY_UPDATE_IND: PHY upgrade negotiation. If your device requests 2M PHY but the peer doesn’t support it, you’ll see the rejection here.
  • LL_TERMINATE_IND: The disconnection PDU. The ErrorCode field is the single most useful byte for diagnosing unexpected drops. Common values: 0x08 (Connection Timeout, supervision timer expired), 0x13 (Remote User Terminated), 0x3E (Connection Failed to be Established).

Wireshark Display Filters That Actually Save Time

Don’t scroll through thousands of advertising PDUs manually. Build a filter library:

FilterPurposeWhen to use
btle.advertising_address == aa:bb:cc:dd:ee:ffIsolate one device’s advertisingInitial triage, finding your device in a crowded capture
btle.data_header.llid == 0x03Show only LL Control PDUsDebugging parameter updates, PHY changes, disconnections
btatt.opcode == 0x01ATT Error Responses onlyFinding GATT operation failures fast
btatt.handle == 0x0015Filter by attribute handleTracing reads/writes to a specific characteristic
btle.crc.incorrect == 1CRC failuresInvestigating RF issues or packet corruption
btl2cap.cid == 0x0004ATT channel traffic onlyCutting L2CAP signaling noise to focus on GATT
btle.control_opcode == 0x02LL_TERMINATE_INDFinding disconnection events and their reason codes
frame.time_delta > 0.05Gaps > 50ms between framesSpotting missed connection events or scheduling issues

Combine filters with && and ||. For example, btatt.opcode == 0x01 && btatt.error_code == 0x05 finds all Insufficient Authentication errors in one shot.

Three Bugs You Can Diagnose in Under a Minute

Device fails to connect: Filter for CONNECT_IND from the central. If it’s absent, the central never received the advertisement (check advertising interval and channel). If present but no data channel packets follow, compare the supervision timeout value in CONNECT_IND against the time gap. The peripheral likely missed the first connection event window.

GATT operation returns an error: Filter with btatt.opcode == 0x01. The Error Response packet identifies the exact handle and error code. 0x05 (Insufficient Authentication) means the characteristic requires an encrypted link but pairing hasn’t completed. Look for SMP packets earlier in the trace.

Connection drops unexpectedly: Filter with btle.control_opcode == 0x02. The LL_TERMINATE_IND packet’s reason code is definitive. 0x08 means the supervision timer fired, meaning one side stopped hearing the other. Count connection events preceding the drop to determine if the issue is periodic (suggesting scheduling conflicts) or sudden (suggesting interference). For more systematic approaches to connection troubleshooting, see our guide on debugging BLE connection failures.

From Setup to Expert: Making the nRF Sniffer Your Go-To BLE Debug Tool

A working nRF Sniffer setup fundamentally changes how you debug BLE. Issues that take hours to reproduce and reason about through log analysis become visible in seconds in a pcap. The investment is a $40 dev kit and an hour of setup time.

Start by capturing a known-good connection between your device and a reference phone. Save that pcap as your baseline. When a bug report comes in, capture the failing scenario and diff the two traces. The divergence point is almost always your root cause.

From here, explore connection parameter negotiation in depth with our guide to BLE connection parameters, or reference the Bluetooth Core Specification v5.4 (Vol 6, Part B) for the full link-layer state machine. The better you know the spec, the faster Wireshark’s dissector becomes a powerful debugging tool rather than a wall of hex.


Hubble Network enables BLE connectivity from any device directly to satellites—no gateways, no infrastructure. See how it works →