TL;DR: nRF Connect for Mobile is the fastest way to debug a Flutter BLE app because it gives you a known-good reference central. If your peripheral works in nRF Connect but breaks in your flutter_blue_plus code, the bug is in your app — not your firmware. Pair nRF Connect with verbose FBP logs and an HCI snoop capture and almost every BLE bug becomes reproducible in minutes instead of hours.
BLE bugs are a special kind of miserable. The radio is invisible, the stack is asynchronous, and the error messages are famously unhelpful — android-code: 133, a characteristic that reads fine in one app and returns null in yours, notifications that silently never arrive, or a connection that works on your phone but not the tester's. You can lose an entire day guessing which layer is lying to you.
The single biggest mistake Flutter developers make when debugging BLE is treating their app as the only variable. A BLE connection has at least three independent moving parts: your Dart code, the platform's native GATT stack (which behaves very differently on iOS versus Android), and the peripheral's firmware. When something breaks, you need a way to hold two of those constant so you can find the third.
That is exactly what nRF Connect for Mobile gives you. This guide walks through the triage workflow we use on real projects: how to use nRF Connect as a reference central, how to turn on verbose flutter_blue_plus logging, how to capture and read HCI snoop logs, and how to fix the handful of errors that cause most BLE pain.
Why nRF Connect is always your first stop
nRF Connect for Mobile is a free, generic BLE central app from Nordic Semiconductor, available on both iOS and Android. It can scan for devices, parse advertisement data, plot an RSSI graph, connect, discover services and characteristics, read and write values, subscribe to notifications and indications, and — crucially — show you a timestamped Logger view of every single operation.
Think of it as a battle-tested BLE client written by the company that makes the chips. It is the closest thing you have to "ground truth." That makes it perfect for the one question that resolves most bugs:
Does your peripheral behave correctly with a reference central?
- If it works in nRF Connect but fails in your Flutter app, the bug is in your Dart code or your
flutter_blue_plus usage. Stop debugging firmware.
- If it fails in nRF Connect too, the bug is in your peripheral firmware (or its GATT table), and no amount of Dart will fix it.
This one decision saves more time than any other debugging technique. Before you touch your app, reproduce the problem — or fail to — in nRF Connect.
The isolation test: the most important 60 seconds
Here is the minimal sequence. Open nRF Connect and:
- Scan and find your device by name or advertised service UUID. If it does not show up here, the problem is advertising/firmware or your scan filters and permissions — not your connection code.
- Connect. Watch how long it takes and whether it drops.
- Expand the service and confirm your characteristics exist with the UUIDs and properties (read/write/notify) you expect.
- Read the characteristic and look at the raw bytes.
- Toggle notifications (the triple-arrow icon) and confirm values stream in.
Now do the exact same thing in your app. The first step that diverges is your bug.
One gotcha that trips up everyone: the device identifier is not the same across platforms. On Android you get the hardware MAC address; on iOS, Core Bluetooth hands you a per-app, rotating UUID instead — you never see the MAC. So "the address is different in nRF Connect than in my app" is expected behavior on iOS, not a bug. If you want the deeper model here, see our BLE GATT profiles explainer.
Read the data exactly as it is sent
nRF Connect displays every value as raw hex, which is the single best way to catch encoding bugs. Say a battery characteristic shows (0x) 64-00. That is two bytes, little-endian, so the real value is 0x0064 = 100, not 0x6400 = 25600. Garbled readings in your app are almost always an endianness or offset mistake, and nRF Connect shows you the truth on the wire so you can match it byte-for-byte in Dart:
// nRF Connect shows: (0x) 64-00 → two bytes, little-endian
final raw = characteristic.lastValue; // List<int>, e.g. [0x64, 0x00]
final value = raw[0] | (raw[1] << 8); // 100, not 25600
If your firmware is not ready yet, you can flip nRF Connect around and use its GATT Server tab to emulate a peripheral — add standard services from its catalog or define a custom one — so you can build and test your Flutter central against a stable target before any hardware exists.
Mirror nRF Connect inside your app: verbose FBP logging
Once you know what should happen, turn on verbose logging in flutter_blue_plus so you can see what your app actually does. Set the log level once at startup (debug builds only):
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
void main() {
// Logs every operation, tagged [FBP], including bytes in and out.
FlutterBluePlus.setLogLevel(LogLevel.verbose, color: true);
runApp(const MyApp());
}
Every FBP operation now prints to the console with a [FBP] tag and visual indicators for data direction. Read it side-by-side with the nRF Connect Logger and look for three things: the connection state transitions, the negotiated MTU, and the actual bytes being written and received. Nine times out of ten the divergence is obvious once both logs are in front of you.
When a call fails, catch the exception and print the native error code rather than a generic message — that code is what you will actually search for:
try {
await device.connect(timeout: const Duration(seconds: 10));
} on FlutterBluePlusException catch (e) {
// e.code surfaces the platform error. 133 on Android is the classic one.
debugPrint('connect failed: code=${e.code} — ${e.description}');
}
Stuck on a bug that only reproduces on one specific phone? That is exactly the kind of platform-stack edge case we dissect in the BLE Flutter course — with a complete, production-ready logging and error-handling layer you can drop into any app. Get the full debugging toolkit →
When the app log is not enough: HCI snoop captures
Sometimes you need to know whether a packet even left the phone. That is what an HCI snoop log answers — it records the raw Host Controller Interface traffic between the OS and the Bluetooth radio, below your app and below the framework.
On Android: enable Developer Options, turn on "Enable Bluetooth HCI snoop log," then toggle Bluetooth off and on so it takes effect. Reproduce the bug, then pull the capture — the modern, root-free path is to grab a bug report (adb bugreport) and extract the btsnoop_hci.log, or on some devices adb pull /data/misc/bluetooth/logs/btsnoop_hci.log. Open the resulting file in Wireshark, which auto-detects the btsnoop format.
On iOS/macOS: the supported tool is PacketLogger, included in Apple's Additional Tools for Xcode download. It captures Bluetooth HCI traffic and exports to a Wireshark-compatible format.
In Wireshark you can filter to the ATT/GATT protocol and watch the real exchange: the service discovery, the CCCD write that enables notifications, your characteristic write, and the device's response (or lack of one). This settles "did my write actually go out, and did the peripheral ACK it?" with zero ambiguity.
The bugs you will actually hit
GATT error 133 (Android). This is a generic "something went wrong" code on Android, not a specific failure. The usual triggers are connecting too soon after a scan, the BLE stack still being busy from a previous connection, or never disconnecting cleanly. Stop scanning before you connect, add a short backoff, and wrap connect() in a bounded retry. The mechanics of a robust reconnect loop are covered in depth in our Flutter BLE auto-reconnect guide.
Notifications silently never arrive. The order of operations matters. You must discoverServices() to completion before subscribing, and setNotifyValue(true) is what writes the CCCD (descriptor 0x2902) that actually enables the stream. Subscribe before telling the device to start sending, and remember some peripherals will not push notifications until you are bonded:
await device.discoverServices(); // must finish first
final ok = await characteristic.setNotifyValue(true); // writes the CCCD
characteristic.onValueReceived.listen((value) {
debugPrint('notify: $value');
});
This is a partial example. A production implementation also re-subscribes after every reconnect, deduplicates the initial lastValue emission, and handles devices that report a characteristic as notifiable but expose no CCCD. We build that complete, reconnect-safe notification layer step by step in the course.
Stale services after a firmware update (Android). Android aggressively caches a device's attribute table. After you change the firmware's GATT layout, your app may keep discovering the old services while nRF Connect shows the new ones. Toggle Bluetooth, "forget"/re-pair the device, or clear the GATT cache to force a fresh discovery.
Truncated data. The default MTU is 23 bytes, leaving only 20 bytes of usable payload. If your longer writes or notifications are getting cut off, you need to negotiate a larger MTU on Android (iOS negotiates automatically). See our deep dive on MTU negotiation and large data transfer.
"Write succeeds but nothing happens." Check whether the characteristic expects write with response versus write without response — nRF Connect shows the supported write types, and sending the wrong one can fail silently.
Nothing scans at all. On Android 12+ a missing BLUETOOTH_SCAN / BLUETOOTH_CONNECT runtime permission makes scanning return an empty list with no error. Always rule this out first — our Android & iOS permissions guide covers the full manifest and runtime setup.
A repeatable triage checklist
When a BLE issue lands on your desk, work it in this order:
- Reproduce in nRF Connect. Works there? The bug is in your app. Fails there? It is firmware.
- Turn on
LogLevel.verbose and read the FBP log against the nRF Connect Logger.
- Confirm permissions and that the device actually appears in a scan.
- Verify service discovery completes before any read/write/subscribe.
- Compare the raw bytes and the negotiated MTU on both sides.
- Still stuck? Capture an HCI snoop log and read the ATT layer in Wireshark.
Want the whole workflow as a ready-to-use harness? The Complete Flutter BLE Course ships a production debugging layer — structured logging, typed error handling, and a reconnect-safe GATT wrapper — plus the firmware-side test rig so you can isolate bugs in seconds. Start building reliable BLE apps →
Related guides
FAQ
Is nRF Connect for Mobile free, and which platforms support it?
Yes. nRF Connect for Mobile is free from Nordic Semiconductor and available on both Android and iOS. The Android version exposes slightly more (such as a full configurable GATT server and richer logging), but both are excellent reference centrals.
My device works in nRF Connect but not in my Flutter app. What now?
That result is genuinely good news — it proves the firmware and GATT table are fine, so the bug is in your Dart code or flutter_blue_plus usage. Turn on LogLevel.verbose, confirm discoverServices() completes before any operation, and compare your byte parsing against the raw hex nRF Connect shows.
How do I fix Android GATT error 133?
Error 133 is a generic Android failure, usually caused by connecting while the stack is busy. Stop scanning before connecting, add a short delay, and retry the connection a few times with backoff. If it persists, test with autoConnect: true and make sure you are disconnecting cleanly. See our auto-reconnect guide for a robust pattern.
Why are my notifications not arriving even though setNotifyValue returned true?
A true return only means the CCCD write was accepted, not that the device is sending. Confirm you subscribed after service discovery, that you are listening to onValueReceived before triggering data, and that the peripheral does not require bonding first. Verify in nRF Connect that notifications stream there.
Do I need root or a jailbreak to capture HCI logs?
No. On Android, the Developer Options "Bluetooth HCI snoop log" toggle plus a bug report gets you the capture without root on most modern devices. On iOS, Apple's PacketLogger (from Additional Tools for Xcode) is the supported, no-jailbreak path.
Summary
Debugging Flutter BLE does not have to be guesswork. Use nRF Connect for Mobile as a reference central to instantly split app bugs from firmware bugs, mirror it inside your app with verbose flutter_blue_plus logging, and drop down to an HCI snoop capture in Wireshark when you need to see the raw packets. With those three tools and the triage checklist above, the usual suspects — error 133, missing notifications, stale services, and truncated data — become quick, repeatable fixes instead of all-day mysteries.
If you want the complete, production-grade version of this workflow — a reusable logging and error-handling layer, a reconnect-safe GATT wrapper, and the firmware test rig that makes isolation trivial — that is exactly what we build, end to end, in the Complete Flutter BLE Course. Join the course and ship reliable BLE apps →