
简介面向安卓开发者和物联网初学者的蓝牙 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_SCAN和BLUETOOTH_CONNECT运行时权限。iOS平台相对封闭但也很干净。CoreBluetooth把逻辑分成了Central和Peripheral两大角色连MTU都不用自己协商——系统会自动根据双方能力调整但代价是你拿不到底层RSSI的实时回调必须做额外处理。iOS还有一个特点是如果你不做后台模式配置App一退后台连接就会断开这对调试过程很致命。Windows平台最尴尬。WinForms项目如果要实现BLE通信.NET Framework 4.7.2以下版本只能调用WinRT APIWindows.Devices.Bluetooth但那个API在WinForms里用起来需要跨异步边界打包部署还要求Windows 10 1803以上。社区里常用的第三方库有ble.net封装了WinRT也有基于32feet.NET做经典蓝牙的老牌方案。但真正面向BLE的、跨平台的、源码开放的选择实在太少。Linux平台相对简单直接调BlueZ用bluetoothctl做交互式命令或者用D-Bus接口自己写工具。我在调试时经常会用bluetoothctl关闭经典蓝牙只保留BLEbr-keep-bredr策略这样能避免经典蓝牙协议的干扰。这部分操作在源码里可以做一层封装但Windows不是很好模拟。3.2 我的最终选择跨平台分层架构考虑到要同时覆盖Android和Windows两大主流调试场景又不想维护两套完全不同的UI我最终选用了这样的分层方案底层协议层用C封装一套跨平台的GATT客户端核心Android上通过JNI调用系统的BluetoothGattWindows上通过C/WinRT调用WinRT API。这样核心业务逻辑扫描队列、连接状态机、数据缓存、日志格式化只写一次。UI层Android端用KotlinJetpack ComposeWindows端用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字节MTUnRF52832支持247字节。注意协商MTU要在服务发现之后、数据交互之前做。代码里我还加了个倒计时逻辑如果协商超时比如对端不响应自动回退到默认MTU并弹提示防止卡住后续流程。4.3 服务发现与特征值读写流程服务发现是BLE调试里最直观的解剖过程。源码里我实现了一键遍历所有服务、特征值、描述符的树形展示。这个功能看起来简单但实现上有不少优化点服务发现虽然走系统API但你拿到的BluetoothGattService对象如果只保存引用连接断开之后内存就释放了。我选择把整个服务树转成自定的数据模型ServiceInfo、CharacteristicInfo、DescriptorInfo这样即使断连重连UI上还能保留上一次的结构快照方便对照看差异。特征值读写提供了三种操作类型读Read、写Write带响应、写无响应Write Without Response。这三种对应GATT协议里的Read Request、Write Request和Write 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 17Gradle 8.2。Windows端是Visual Studio 2022 Qt 6.5如果要在.NET Framework 4.7.2项目里做BLE建议直接引用ble.net库并查看它的源码实现。权限配置是第一个坑。Android 12API 31以上BLUETOOTH_SCAN和BLUETOOTH_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)setCharacteristicNotification和writeDescriptor是两个独立操作必须先调用前者再写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确认特征值属性里有WriteRSSI始终为0未开启RSSI回调、设备已断开扫描阶段显示RSSI连接后的RSSI需手动readRemoteRssiiOS连接正常但Android连不上连接参数严格度差异、Android要求ioCapability匹配调大connInterval到30ms以上检查配对模式数据乱序、分包未做粘包处理、MTU协商后未重新分包按时间间隔缓存拼接根据实际MTU手动分帧6.2 关于连接参数与iOS规范的补充如果你是拿iOS设备做主机连接参数的约束比Android严格得多。搜索热词里的iOS BLE连接参数规范就是指这个CoreBluetooth要求Connection Interval必须在30ms左右的可接受范围内否则系统会拒绝连接或者把连接参数向系统偏好调整。也就是说如果你的从机设备把连接间隔设成了7.5msAndroid会允许iOS端可能会出现连接不稳定或延迟偏高的现象。调试助手源码里我应该提供一个连接参数只读展示的功能把当前连接协商到的连接间隔、从机延迟、超时时间直接显示出来。当你在iOS和Android上对比同一台设备时这个功能能快速发现参数协商差异避免把问题误判为硬件故障。比如我用ESP32做从机时默认的连接间隔是30msiOS连接后会被系统拉到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固件时把ATGATTTOOL、ATBLESTARTADV这类指令全部做成了按钮整个过程完全不需要键盘输入。第二个可以扩展的方向是日志的标准化导出。目前我导出的日志格式是时间戳方向Hex数据的CSV这个格式可以直接丢进Wireshark配合btle插件分析也方便和同事同步问题现场。如果你需要对接自动化测试框架也可以把日志输出改成JSON。第三个方向是把工具从调试延伸到产测。我在产线上用过一段时间发现只要加上简单的PASS/FAIL判定逻辑比如收到特定应答帧即判定通过它可以瞬间变成一个简易的BLE产测工具。源码里我已经预留了ParseResult接口就是为这个场景准备的。调试工具就是这样看起来只是一个小助手但它背后承载的是整个BLE开发流程里的信息可视化和问题定位效率。真正做下来你会发现实现它其实不难难的是对协议细节的把控和对调试场景的理解。希望这套源码能帮你省下一些弯路也欢迎你在使用中继续改进它。本文还有配套的精品资源点击获取