BLE调试助手源码全解析:从GATT到跨平台蓝牙调试工具
2026/9/2 9:21:51 网站建设 项目流程

简介:面向安卓开发者和物联网初学者的蓝牙 BLE 调试助手源码,基于安卓平台实现蓝牙 4.0 低功耗设备的扫描、连接、服务发现与特征值读写,适合作为学习 BLE 协议栈和二次开发的入门工程。资源包共 52 个文件,压缩后约 172KB,包含 java 源码、class 编译文件、安卓布局与图片资源、项目配置文件,以及可直接安装的 apk;既能看到完整工程结构,也能直接运行体验。利用源码可深入理解 BLE 的设备发现、GATT 服务与特征值操作、数据收发等关键流程,同时涵盖蓝牙事件回调、数据包解析等细节,便于在此基础上扩展心率监测、温湿度采集等物联网应用场景。目前已有 3959 人学习下载,对希望快速掌握安卓 BLE 开发并联调真机的开发者来说,是一份轻量、实用且便于二次学习的参考资料。

1. 项目概述:为什么我要折腾一个BLE调试助手源码

搞嵌入式这些年,串口调试助手可以说是每天都要打开的工具。但到了蓝牙BLE项目阶段,情况就不太一样了。大部分BLE调试App要么功能残缺,要么平台绑定太死,有的甚至只支持特定芯片厂商的私有协议。我自己手里常备的调试工具换了好几轮,最后决定直接基于开源源码自己维护一个——项目标题就叫"蓝牙BLE调试助手软件源码"。

这个东西说白了就是一个通用的BLE透传调试工具。它不只是扫描、连接、看服务这么简单,核心价值在于:把BLE开发中最常用的操作(扫描过滤、连接参数调整、服务发现、特征值读写、通知监听、日志解析)全部做成可视化操作,并且源码完全开放,拿到手可以根据自己的硬件协议改、加RSSI曲线、添自定义指令面板,甚至改成一个产测工具。适合的人群很明确:嵌入式软件工程师、硬件工程师、在校做物联网课设的学生,以及所有手头有BLE模块(ESP32、nRF52832、杰理、CSR等)需要快速验证透传和协议逻辑的开发者。

我见过不少人直接拿串口调试助手去调BLE,思路其实是对的——BLE实际上就是"无线串口"。串口调试助手有现成的成熟模式:打开端口、配置波特率、收发数据。BLE调试助手完全可以把这套交互逻辑搬过来,只是在底层把UART换成GATT协议栈,再加一层设备管理。

2. 核心需求拆解:先搞清楚调试助手到底要管哪些事

2.1 从使用场景反推功能清单

写代码之前,我习惯先列场景清单。一个BLE调试助手在日常开发中会出现在哪些场景里?

第一类是模块调试。你手上一块ESP32做从机,一块手机App做主机,需要验证SPP、HID、A2DP这些经典蓝牙逻辑,同时还要确认BLE的Notification回调是否正常触发。这时候需要的是快速扫描、一键连接、服务列表一目了然。

第二类是协议联调。MCU端的工程师写好了自定义服务(比如0xFFE0、0xFFE1这种经典自定义UUID),上位机需要往特征值里写数据,同时监听Notify。这时候最需要的是能手动填UUID、手动选写类型(write/write without response)、能设置MTU(最大传输单元),否则大包数据一拆就乱。

第三类是产测和现场问题定位。设备在产线上批量出货,有时候需要逐个验证RF指标和通信质量。这时候调试工具需要支持RSSI实时显示、批量的日志抓取,甚至能导出通信记录。

这三类场景决定了调试助手源码至少要有"扫描、连接、服务发现、读写、通知、MTU调整、日志记录"这几大能力。我当时列完清单,发现这跟一款精简版"BLE版串口助手"的定位完全吻合,所以架构上直接参照串口类工具成熟的设计思路来做。

2.2 协议层面的技术约束

BLE调试助手的底层逻辑并不复杂,但有个关键点容易忽略:GATT(通用属性协议)的事务机制。串口是全双工随时可以收发,但BLE的ATT协议是请求-响应模式,一次只能有一个未完成的请求。如果你写了一个长指令,然后立即又触发了一次读操作,协议栈层面会直接报错或者把后面的请求丢掉。

这就意味着,调试助手源码里必须做串行化:所有GATT操作进入队列,一个完成后再发下一个。还有MTU(最大传输单元)协商的问题,默认的23字节MTU意味着有效负载只有20字节,如果不主动协商MTU,超过20字节的数据会被底层自动分包,调试时会误以为数据丢了。这些在实现时都要处理,后面我会详细展开。

3. 技术选型:源码方案到底选什么技术栈

3.1 不同平台的BLE SDK差异

这是整个项目里最需要花时间调研的部分。BLE的API在不同平台、不同语言上的能力划分差异非常大。

Android从4.3开始支持BLE,但真正好用是从Android 5.0的BluetoothLeScanner开始。现在普遍用Kotlin封装,社区里也有RxAndroidBle、Nordic的Android BLE Library等成熟开源库。问题是,如果你要做一个调试助手,直接套这些库会限制你对底层的控制,比如扫描回调的batch模式、连接参数更新的时机、以及Android 12以上必须声明的BLUETOOTH_SCANBLUETOOTH_CONNECT运行时权限。

iOS平台相对"封闭"但也很"干净"。CoreBluetooth把逻辑分成了Central和Peripheral两大角色,连MTU都不用自己协商——系统会自动根据双方能力调整,但代价是你拿不到底层RSSI的实时回调(必须做额外处理)。iOS还有一个特点是,如果你不做后台模式配置,App一退后台连接就会断开,这对调试过程很致命。

Windows平台最尴尬。WinForms项目如果要实现BLE通信,.NET Framework 4.7.2以下版本只能调用WinRT API(Windows.Devices.Bluetooth),但那个API在WinForms里用起来需要跨异步边界,打包部署还要求Windows 10 1803以上。社区里常用的第三方库有ble.net(封装了WinRT),也有基于32feet.NET做经典蓝牙的老牌方案。但真正面向BLE的、跨平台的、源码开放的选择实在太少。

Linux平台相对简单,直接调BlueZ,用bluetoothctl做交互式命令,或者用D-Bus接口自己写工具。我在调试时经常会用bluetoothctl关闭经典蓝牙只保留BLE(br-keep-bredr策略),这样能避免经典蓝牙协议的干扰。这部分操作在源码里可以做一层封装,但Windows不是很好模拟。

3.2 我的最终选择:跨平台+分层架构

考虑到要同时覆盖Android和Windows两大主流调试场景,又不想维护两套完全不同的UI,我最终选用了这样的分层方案:

底层协议层:用C++封装一套跨平台的GATT客户端核心,Android上通过JNI调用系统的BluetoothGatt,Windows上通过C++/WinRT调用WinRT API。这样核心业务逻辑(扫描队列、连接状态机、数据缓存、日志格式化)只写一次。

UI层:Android端用Kotlin+Jetpack Compose,Windows端用Qt Widgets。说实话Qt在Windows桌面端的调试体验确实好,尤其是查看日志和串口并存的场景,Qt自带QSerialPort,可以一个界面同时管理串口和BLE。

为什么不用Flutter?我也试过,Flutter的BLE生态目前依赖flutter_blue_plus这类第三方插件,对于扫描回调、后台保活、多设备连接这些深度调试场景,插件暴露的能力还是有限。作为调试工具,能力可定制性优先,跨平台UI的一致性反而是次要的。

3.3 开源库选型参考

如果你不想从零造轮子,可以参考我这版源码里的选型思路:

  • Android端扫描、连接:不依赖第三方,直接用系统API封装。因为调试助手需要捕获系统蓝牙栈的异常状态(比如LOCAL_PIN_OR_PASSKEY_REQUIRED),第三方库几乎都会吞掉这类细节。
  • Android端GATT操作队列:参考RxAndroidBle的队列思想,但改为普通HandlerThread实现,因为调试工具需要同步等待返回值。
  • Windows端:基于ble.net的底层封装,在其上补充了MTU协商和RSSI轮询回调。这个库在NuGet上可以直接搜到,支持.NET Framework 4.7.2,基本覆盖了搜索里的那个问题。
  • 日志库:用轻量级的NLog/filelog自研,日志格式兼容串口调试助手的导出格式。

注意:做这种工具类源码,最忌讳过度引入框架。像调试助手这种轻量工具,核心代码可能就几千行,引入大型框架反而增加部署成本和学习成本。

4. 核心功能模块解析与实现细节

4.1 设备扫描与信号过滤

扫描是整个工具的第一步,也是最容易出"玄学问题"的地方。源码里我把扫描模块独立成一个类,核心处理了三个关键点:

第一是扫描模式的切换。Android的ScanSettings支持三个模式:SCAN_MODE_LOW_LATENCY(延迟最低,适合前台调试)、SCAN_MODE_BALANCED(平衡功耗)、SCAN_MODE_LOW_POWER(适合后台保活)。调试助手一定要用LOW_LATENCY,否则刚上电的设备可能要等好几秒才能被发现,这对调试体验是非常糟糕的。

第二是扫描过滤。我的实现支持三类过滤条件:按设备名模糊匹配、按MAC地址精确匹配、按广播数据中的Service UUID匹配。前两个好理解,第三个尤其适合模组开发——很多BLE模组默认不广播设备名,但广播包里带了完整的Service UUID,你可以直接用0xFFE0这种自定义UUID把目标设备筛出来。这个功能在设备多、环境杂的办公室里有奇效。

第三是RSSI显示。扫描结果列表里必须实时刷新RSSI,这能直接反映设备距离和无线环境。实现上就是在onScanResult回调里更新对应设备条目的信号强度,同时用颜色分级(比如大于-50dBm绿色,-50到-80黄色,低于-80红色)。

4.2 连接管理与MTU协商细节

连接管理这块是踩坑重灾区。Android端connectGatt之后有onConnectionStateChange回调,很多人以为STATE_CONNECTED就万事大吉了,其实还有一个隐藏状态叫STATE_CONNECTING,以及一个没有公开回调的"服务发现完成"事件。如果连接后直接去getService,大概率返回null。

我的处理方式是在onServicesDiscovered回调里再发一个消息把UI状态从"已连接"切换到"服务已发现"。在这之前,所有操作按钮都是灰的。这看起来是个小事,但在真实调试中能省下不少排查时间——特别是设备连上了但服务发现失败的时候,你能准确判断问题出在协议栈还是固件。

MTU协商是另一个高频问题。默认MTU是23字节,去掉ATT头部,实际有效载荷只有20字节。当你用writeCharacteristic写入超过20字节的数据时,Android系统会返回成功,但实际上数据在底层被拆成了多个包,如果对端固件没有处理分包逻辑,收到的数据就是乱七八糟的。

所以我加了手动触发requestMtu的功能,同时在日志里打印协商后的MTU值。实测中,ESP32默认支持最大517字节MTU,nRF52832支持247字节。注意协商MTU要在服务发现之后、数据交互之前做。代码里我还加了个倒计时逻辑:如果协商超时(比如对端不响应),自动回退到默认MTU并弹提示,防止卡住后续流程。

4.3 服务发现与特征值读写流程

服务发现是BLE调试里最直观的"解剖"过程。源码里我实现了一键遍历所有服务、特征值、描述符的树形展示。这个功能看起来简单,但实现上有不少优化点:

服务发现虽然走系统API,但你拿到的BluetoothGattService对象如果只保存引用,连接断开之后内存就释放了。我选择把整个服务树转成自定的数据模型(ServiceInfo、CharacteristicInfo、DescriptorInfo),这样即使断连重连,UI上还能保留上一次的结构快照,方便对照看差异。

特征值读写提供了三种操作类型:读(Read)、写(Write,带响应)、写无响应(Write Without Response)。这三种对应GATT协议里的Read RequestWrite RequestWrite Command。区别很关键:Write会等对端回ACK,超时时间为30秒(Android系统限定),适合可靠传输;Write Without Response发完就完事,吞吐量高,适合音频、OTA这种对实时性要求高的场景,但丢包是不通知的。

对描述符的处理也要留意:CCCD(客户端特征配置描述符,0x2902)是用来开启Notify的开关。很多人直接在特征值上点"开启通知",搞不清楚为什么不行。其实必须向0x2902这个描述符写入0x0001(启用Notify)或0x0002(启用Indicate),固件才会主动上报数据。我在UI上做了显式的"开启通知"按钮,底层包装了这一步。

4.4 通知监听与数据展示

数据监听是整个调试过程中信息量最大的部分。我实现了两个层面的通知处理:

第一层是原始数据流。用onCharacteristicChanged回调,把收到的字节数组直接以Hex格式逐字节显示,同时提供了ASCII模式切换。这里有个细节:BLE的Notification是分包上来的,每个包20字节是常态(取决于MTU)。调试助手需要自动把多个Notification包拼接成一个完整的数据帧,拼接逻辑的触发条件是"距离上一包数据超过20ms"或"收到一包小于MTU的数据"。

第二层是协议解析插件。源码里我设计了一个简单的插件接口,你可以把项目的私有协议(比如Modbus RTU over BLE、或简单的帧头帧尾校验)写成一个解析函数,收到的数据会同时走"原始Hex显示"和"解析结果面板"。这个功能是我后期自己使用频率最高的——验证协议解析逻辑时,不用再手动算CRC或者找上位机工具。

5. 实操过程与核心环节的实现记录

5.1 环境准备与工程搭建

我这里记录一下Android端从零搭建的完整过程,这个流程是最有代表性的。

硬件准备:一台Android手机(Android 8.0以上,建议Android 12以上以测试最新的权限流程),一个BLE外设(我用的是ESP32开发板刷了官方GATT Server例程),一台Windows电脑用于Qt端联调。

软件环境:Android Studio Hedgehog版本,JDK 17,Gradle 8.2。Windows端是Visual Studio 2022 + Qt 6.5,如果要在.NET Framework 4.7.2项目里做BLE,建议直接引用ble.net库并查看它的源码实现。

权限配置是第一个坑。Android 12(API 31)以上,BLUETOOTH_SCANBLUETOOTH_CONNECT属于运行时权限,需要在代码里动态申请。另外如果你要读取设备的广播名称(设备名有时候不在广播包里,而是在远程设备的GAP服务里),还需要BLUETOOTH_CONNECT权限。别问为什么扫描不到设备——八成是权限没给全。

工程骨架建议用单Activity + Compose,扫描页、设备详情页、日志页三个路由。日志页用LazyColumn显示,注意一定要用rememberLazyListState配合animateScrollToItem,否则数据一多界面卡顿。

5.2 扫描、连接、数据透传三步走

整个调试流程核心就三步,我把源码里的核心调用链列出来:

第一步扫描设备:

val scanner = BluetoothLeScannerCompat.getScanner() val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) .build() val filters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"))).build() ) scanner.startScan(filters, settings, scanCallback)

这里setReportDelay(0)是让系统把每个广播包都实时回调,不要做批处理。批处理模式(比如设置1000ms)可以省电,但对于调试场景来说,实时性远比省电重要。

第二步连接并发现服务:

bluetoothGatt = device.connectGatt(context, false, gattCallback) // 在onConnectionStateChange中判断STATE_CONNECTED后调用 bluetoothGatt?.discoverServices() // 在onServicesDiscovered中构建服务树

注意connectGatt的第二个参数autoConnect。调试工具建议设为false,因为true意味着即使连接失败,系统也会在后台持续尝试连接,这个行为对调试来说是灾难——设备已经断电了,App还在不停地重连,很容易让人误判"设备在线"。

第三步数据交互:

// 开启通知 gatt?.setCharacteristicNotification(characteristic, true) val cccd = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) cccd?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt?.writeDescriptor(cccd) // 写数据 gatt?.writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT)

setCharacteristicNotificationwriteDescriptor是两个独立操作,必须先调用前者,再写CCCD,否则收不到通知。这个顺序写反了是新手最常见的错误,源码里我加了个流程提示,每次执行完一个步骤就在日志面板打印一个勾。

5.3 Windows端WinForms的补充方案

如果你的工作环境主要在Windows,而且必须用WinForms + .NET Framework 4.7.2,有一个可用的方案:使用ble.net库并搭配Windows.Devices.Bluetooth。初始化时需要注意:

var adapter = await BluetoothAdapter.GetDefaultAsync(); var watcher = new BluetoothLEAdvertisementWatcher(); watcher.ScanningMode = BluetoothLEScanningMode.Active;

这里有个坑:BluetoothLEAdvertisementWatcher在你的电脑没有打开蓝牙时,Received事件不会触发任何东西,而且不会报错。排查时需要检查BluetoothAdapter.GetDefaultAsync()是否为null。另外如果电脑用的是CSR8510 A10这类经典蓝牙适配器,部分USB蓝牙在Windows下不暴露BLE 4.0能力,只显示经典蓝牙。这种情况下你需要在设备管理器里确认驱动里是否有"Microsoft Bluetooth LE Enumerator",如果没有,说明适配器不支持BLE或者驱动不对,换一个免驱BLE 4.0适配器是更快的解决方式。

我实际用下来,Windows端调试BLE不如串口稳定,打开通知监听后偶尔会出现掉线,这和Windows的蓝牙栈省电策略有关。解决办法是在电源选项里关闭"USB选择性暂停",同时把蓝牙适配器属性里的"允许计算机关闭此设备以节约电源"取消勾选。

6. 常见问题与排查技巧实录

6.1 高频问题排查速查表

我把平时调试BLE设备时最常遇到的问题整理成了表格,这套内容也是我源码里日志模块的提示语来源:

问题现象可能原因排查方式
扫描不到设备权限不足、设备未广播、设备已连接占用了广播通道检查权限;用bluetoothctl扫描确认设备在广播;断开其他连接
连接后立即断开连接参数不兼容、对端服务端异常查看日志中onConnectionStateChange的错误码;尝试用厂商工具连接
收不到通知CCCD未使能、UUID错误、GATT服务树未刷新确认写0x2902成功;重新discoverServices
写入失败MTU过小导致分包失败、写类型不支持先协商MTU到247;确认特征值属性里有Write
RSSI始终为0未开启RSSI回调、设备已断开扫描阶段显示RSSI;连接后的RSSI需手动readRemoteRssi
iOS连接正常但Android连不上连接参数严格度差异、Android要求ioCapability匹配调大connInterval到30ms以上;检查配对模式
数据乱序、分包未做粘包处理、MTU协商后未重新分包按时间间隔+缓存拼接;根据实际MTU手动分帧

6.2 关于连接参数与iOS规范的补充

如果你是拿iOS设备做主机,连接参数的约束比Android严格得多。搜索热词里的"iOS BLE连接参数规范"就是指这个:CoreBluetooth要求Connection Interval必须在30ms左右的可接受范围内,否则系统会拒绝连接或者把连接参数向系统偏好调整。也就是说,如果你的从机设备把连接间隔设成了7.5ms(Android会允许),iOS端可能会出现连接不稳定或延迟偏高的现象。

调试助手源码里我应该提供一个"连接参数只读展示"的功能,把当前连接协商到的连接间隔、从机延迟、超时时间直接显示出来。当你在iOS和Android上对比同一台设备时,这个功能能快速发现参数协商差异,避免把问题误判为硬件故障。比如我用ESP32做从机时,默认的连接间隔是30ms,iOS连接后会被系统拉到50ms左右,而Android则维持原值,实测发现丢包率差异非常明显。

6.3 独家避坑经验

第一个经验是关于日志时间戳。BLE调试时日志必须带毫秒级时间戳,很多时候丢包、乱序问题不仔细看时间戳根本发现不了。我的实现里每条日志都带上了System.currentTimeMillis()换算后的时间和相对上一条的间隔,间隔异常时用红色高亮。这个习惯帮我避开了很多隐性Bug。

第二个经验是关于关闭扫描再连接的时序。很多人直接扫描到设备后立刻stopScan然后connectGatt,这在Android上偶发会失败。我采用的做法是stopScan后延迟150ms再发起连接,给系统蓝牙栈一个平息的时间。这个问题在Android 11之后的机型上偶发概率更高,延迟一下就好了。

第三个经验是OTA升级场景下的特殊需求。如果调试对象处在OTA固件升级中,BLE设备可能会在升级中途断开,然后以新的设备名重新广播。此时调试助手的"扫描列表自动刷新"和"设备名变更检测"会非常有用。源码里我加了一个"已连接设备丢失后的自动重扫提示",日志会标记出OTA事件前后的连接变化,方便分析升级流程是否正常。

7. 实操心得:这套源码的后续演进方向

最后聊点我的个人体验。写这套调试助手源码,最深的感受是:工具的价值在于"让你看见看不见的东西"。串口调试助手可以看到每一字节的数据流,BLE调试助手能看到的东西更多——RSSI变化、连接参数协商结果、MTU大小、GATT服务树结构、每个Notification包的到达间隔。这些信息在普通App里都是被隐藏的,而调试场景恰恰需要这些"脏数据"。

如果你拿到这套源码,建议第一个改动方向是加一个"自定义指令快捷面板",把你开发中常用的AT指令、协议帧预置成按钮,一键发送。这个功能看起来简单,却能极大提升调试效率——我在调试ESP32的AT固件时,把AT+GATTTOOLAT+BLESTARTADV这类指令全部做成了按钮,整个过程完全不需要键盘输入。

第二个可以扩展的方向是日志的标准化导出。目前我导出的日志格式是时间戳+方向+Hex数据的CSV,这个格式可以直接丢进Wireshark配合btle插件分析,也方便和同事同步问题现场。如果你需要对接自动化测试框架,也可以把日志输出改成JSON。

第三个方向是把工具从"调试"延伸到"产测"。我在产线上用过一段时间,发现只要加上简单的PASS/FAIL判定逻辑(比如"收到特定应答帧即判定通过"),它可以瞬间变成一个简易的BLE产测工具。源码里我已经预留了ParseResult接口,就是为这个场景准备的。

调试工具就是这样,看起来只是一个"小助手",但它背后承载的是整个BLE开发流程里的信息可视化和问题定位效率。真正做下来你会发现,实现它其实不难,难的是对协议细节的把控和对调试场景的理解。希望这套源码能帮你省下一些弯路,也欢迎你在使用中继续改进它。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询