拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Qt的Android BLE调试助手源码解析与工程实践

简介这是一款面向嵌入式开发工程师与物联网硬件调试人员的BLE低功耗蓝牙串口调试助手基于Qt框架开发专为Android平台适配解决传统手机蓝牙无法直连BLE设备的核心痛点。资源包共50个文件包含26张界面图标与状态图png、8个QSS样式表css用于UI主题定制、2个核心源码文件mainwindow.cpp与main.cpp、1个主窗口定义ui、1个资源注册文件qrc及1个可直接安装的APK调试应用整体体积13.84MB结构清晰便于二次开发与功能扩展。已有6448人学习下载适用于智能家居、穿戴设备等BLE终端的通信协议验证与连接排错。用户可直接部署APK进行现场调试同时通过源码完整掌握BLE扫描、连接、服务发现、特征值读写及连接状态日志输出等关键流程所有蓝牙交互细节实时显示于界面显著提升开发调试效率与问题定位能力。 做BLE调试工具这件事圈子里的朋友十有八九都绕不开。市面上的蓝牙调试App要么功能固定没法定制要么收费还带广告真正能让你放开手脚改界面的没几个。所以我看到这个BLE低功耗蓝牙调试助手(QT)Android源码.zip项目时还挺感触的——它是一份可以直接编译运行的安卓端BLE调试助手完整源码基于Qt框架实现覆盖了设备扫描、连接、服务发现、特征读写、通知订阅这些调试硬件必备的功能。它解决的痛点很直接当你需要调一个BLE外设比如心率手环、温湿度传感器、蓝牙秤或者验证自家模组的通讯协议时不用依赖别人提供的App直接把源码编译成APK装到手机改起来也方便。可能有人会想Android原生不是有现成的BLE API吗为什么还要用Qt这个问题后文会展开聊。简单说如果你电脑上已经有一套Qt写的PC端调试程序或者团队技术栈偏C那用Qt写安卓端BLE工具核心逻辑可以一套代码两端复用省的时间不是一星半点。这个项目适合谁两类人。一是嵌入式/硬件工程师平时调试低功耗蓝牙模组需要快速搭一个类似LightBlue功能的工具二是Qt开发者想搞懂QtBluetooth模块在Android平台上的用法、权限和坑点这份源码就是现成的学习样板。下面我从拿到压缩包开始按实际开发流程把工程拆开讲。1. 拿到源码后先搞清楚项目解决什么问题1.1 一个BLE调试助手应该具备的完整能力BLE调试助手说白了就是硬件工程师的手机万用表。你不要把它想得多神秘核心能力就是围绕BLE协议栈的四个层级展开扫描Discover、连接Connect、服务发现Service Discovery、特征交互Characteristic Operation。再加上日志查看、MTU调整、数据收发记录这类工程化功能就是一个完整工具了。这个源码项目实际上就把这几块全包了。解压后大致可以看到这样一个结构BleDebugHelper/ ├── BleDebugHelper.pro # Qt工程文件模块依赖在这里声明 ├── android/ │ ├── AndroidManifest.xml # 安卓权限与Activity配置 │ └── res/ # 图标等资源 ├── headers/ │ ├── mainwindow.h # 主窗口扫描列表UI │ ├── blemanager.h # BLE操作核心封装 │ ├── devicescanmodel.h # 扫描结果Model │ └── serviceview.h # 服务/特征详情页 ├── sources/ │ ├── main.cpp # 入口 │ ├── mainwindow.cpp │ ├── blemanager.cpp │ ├── devicescanmodel.cpp │ └── serviceview.cpp └── resources/ └── icons/如果你拿到手的是这个工程打开后建议先别急着编译先花二十分钟把BleManager这个类过一遍——这个类是整个项目的核心后面所有UI操作最终都是调它。1.2 为什么选Qt for Android而不是原生说句实在话做安卓BLE调试工具原生Kotlin确实更正统毕竟Android从4.3就开始支持BLE。但用Qt有几个原生比不了的优势。第一是跨端复用。很多做嵌入式的人调试工具链是Windows/Linux上跑Qt桌面程序如果安卓端也写Qt那么扫描回调、服务发现、特征读写这套业务逻辑可以直接抽出来共用Android端只需要处理平台相关的权限申请。这个项目的BLE核心代码和桌面端兼容基本不需要大改。第二是UI开发效率。调试工具这种内部工具界面不用多漂亮但开发要快。Qt的Widget或QML写列表、表单、日志窗口比原生一顿布局快得多而且一套样式两端统一。第三是QtBluetooth模块本身封装得比较好QLowEnergyController把Android/iOS/macOS的差异遮住了不少。当然这并不代表没有坑后面会逐个讲。原生开发的劣势也很明显你要维护两套代码Android端的蓝牙服务可能要求前台服务权限Android 14以后更严格后台扫描还受限。对于工具类AppQt这种方式能用更低成本达到目的够了。1.3 方案选型手写协议栈还是用现成框架写BLE调试助手还有一个大方向选择是直接用QtBluetooth还是基于原生JNI/Android API手写。有不少人喜欢自己折腾底层觉得直接调Android SDK更灵活。我的建议是除非你要做后台常驻扫描、或者要高度定制蓝牙行为否则直接用QtBluetooth别浪费时间在JNI上做重复劳动。为什么因为BLE协议栈的核心逻辑——广播解析、连接参数协商、GATT读写、属性协议交互——底层平台已经全部处理好了。开发者要做的事情只是调用有限几个API。你花一周用JNI重写一层最后和QtBluetooth的效果几乎一样还多背一堆崩溃日志。不过有一个场景例外如果你需要扫描BLE广播中的原始AdvertisData比如iBeacon或者私有广播协议这时光靠QtBluetooth获取不到完整广播包就得考虑用Android原生回调拿AdvertisData。一般调试助手不需要这个能力如果后面要扩展Beacon检测再走JNI补不迟。2. 开发环境与Android编译配置2.1 Qt for Android开发环境清单既然标题是Android源码那显然不是让你在手机上看代码而是要在开发机编译出APK。先明确这一套环境的版本配比实测下来比较稳的组合是组件推荐版本说明Qt6.5.3 LTS或6.6/6.76.5开始有QPermission权限适配省事Android SDK34或35目标设备多的话建议带Platform 31~34Android NDK25.2.x或26.xQt 6.5官方测试用的是NDK 25.2.9519653JDK17Qt 6要求JDK 17Gradle随Qt自动下载国内网络可能需要换镜像在Qt Creator安装时勾选Qt for Android组件然后菜单 Tools-Options-Devices-Android把SDK、NDK、JDK路径指好后Qt会自动检测并创建好Kit。这里最常见的问题就是NDK版本不匹配报错一般是Cannot find NDK toolchain解法就是换成官方对应的NDK版本。如果你手头是Qt 5.15其实也能跑但Android 12的权限适配会比较痛苦因为Qt 5.15的QtBluetooth没有QBluetoothPermission所有运行时权限都要自己用JNI写。项目里如果用了Qt 6.x建议优先保持。2.2 AndroidManifest.xml权限声明细节这一节是整个项目最容易被忽略、也最容易踩坑的地方。BLE调试助手要正常工作权限必须一次配对否则扫描白屏、连接闪退都是正常现象。我在实际开发中整理了一份可复用的权限配置模板放在AndroidManifest.xml里!-- Android 11及以下传统蓝牙权限 -- uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30/ uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30/ !-- Android 6扫描BLE必须定位权限系统要求 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ !-- Android 12新蓝牙权限 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation/ uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT/这里有几个容易踩的细节ACCESS_FINE_LOCATION不能漏Android 6~11扫BLE必须有即便你的调试工具根本不采集GPS。这是系统的强制策略初衷是防止恶意扫描结果把正常调试工具也误伤了。neverForLocation标志会强制设备不把蓝牙扫描结果用于定位推断。如果你的工具确实和位置无关加上这个可以让部分系统跳过对定位权限的连带要求但在Android 11及以下就算加了它还是要定位权限才能扫。在Android 12及以上设备上运行时还要动态申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT不然连接会直接抛SecurityException。动态申请权限这一块Qt 6.5以后官方推荐用QPermissionvoid MainWindow::requestRuntimePermissions() { #if QT_VERSION QT_VERSION_CHECK(6, 5, 0) QBluetoothPermission btPermission; btPermission.setCommunicationModes(QBluetoothPermission::Access::AllModes); const QListQPermission perms { btPermission, QLocationPermission(QLocationPermission::Access::AccessLocationWhileInUse) }; qApp-requestPermission(perms, this, [](const QListQPermission results) { for (const QPermission p : results) { qDebug() permission result: p; } }); #else // 旧版回退方案用QAndroidJniObject调Activity.requestPermissions #endif }这里把QLocationPermission也一起申请了原因是老系统上扫蓝牙要定位。虽然QBluetoothPermission在Android 12已经能覆盖但为了适配Android 6~11定位权限不能省。2.3 打包架构与APK体积优化默认情况下Qt for Android会把你构建出来的库打包成一个包含所有ABI的大APKarm64-v8a、armeabi-v7a、x86、x86_64全塞进去体积轻松超过100MB。调试助手自己用倒无所谓但如果你要发给其他同事包太大还是尴尬。在Qt Creator里可以通过构建步骤Build Steps里设置ABI来精简armeabi-v7a arm64-v8a两个就够了现在的手机基本都是arm64真计较体积只留arm64-v8a也行。x86可以删掉因为Android模拟器上跑BLE意义不大模拟器也没法直接连真实外设。另一个减小APK体积的办法是在.pro里排除不需要的Qt模块。QtBluetooth依赖的库本身不大但如果你把QML全部装进去体积就上去了。这个项目如果只用Widget模块体积可控。还有个小技巧正式调试用的版本可以在Qt Creator里把Build type改为Release并在项目的pro文件里打开优化选项APK体积和启动速度都会好一些。3. BLE核心功能模块拆解与实现3.1 扫描模块QBluetoothDeviceDiscoveryAgent的使用与去重BLE调试助手的第一步就是把周围的蓝牙外设扫出来。在QtBluetooth里扫描由QBluetoothDeviceDiscoveryAgent负责。void BleManager::startScan(int timeoutMs) { if (m_discoveryAgent) { m_discoveryAgent-stop(); delete m_discoveryAgent; } m_discoveryAgent new QBluetoothDeviceDiscoveryAgent(this); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, BleManager::onDeviceDiscovered); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::errorOccurred, this, BleManager::onScanError); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::finished, this, BleManager::onScanFinished); m_discoveryAgent-setLowEnergyDiscoveryTimeout(timeoutMs); m_discoveryAgent-start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); }几点经验不要用默认的setLowEnergyDiscoveryTimeout值默认值偏长用10秒左右比较合适。扫不到就走人别让用户干等。deviceDiscovered信号在Android上可能对同一个MAC重复发送多次。处理方式是在Model层用一个QHash保存已经出现的地址重复的丢弃。这个细节不处理列表会越刷越长。Android上扫描到的QBluetoothDeviceInfo::address()在某些厂商ROM上可能是空地址返回00:00:00:00:00:00这时建议用deviceUuid()兜底。如果你要连设备优先用完整的deviceInfo对象去做createCentral而不是自己去拼地址。你还可以把扫描结果按RSSI排序把信号强的设备排前面。调试多设备现场非常有用不然一堆同型号设备很难分清该连哪个。3.2 连接模块QLowEnergyController生命周期管理扫描出设备后点击列表项就要发起连接。连接的核心类是QLowEnergyController它负责中央设备Central和外围设备Peripheral的GATT连接管理。m_controller QLowEnergyController::createCentral(deviceInfo, this); connect(m_controller, QLowEnergyController::connected, this, BleManager::onConnected); connect(m_controller, QLowEnergyController::disconnected, this, BleManager::onDisconnected); connect(m_controller, QLowEnergyController::errorOccurred, this, BleManager::onControllerError); connect(m_controller, QLowEnergyController::connectionUpdated, this, BleManager::onConnectionUpdated); m_controller-connectToDevice();我遇到的第一个大坑就是控制器生命周期。QLowEnergyController必须是一个长期存活的对象不能把它的生命周期绑定在一个函数作用域里。如果你在一个临时函数里创建后立刻返回Qt很可能直接把它回收掉然后连接一定失败还不好定位。解决方案就是把它存成类成员并且保证当前页面没有被销毁。连接成功后设备会回调connected信号。注意这里只是GATT连接建立不代表服务已经可用你还需要执行服务发现步骤。有的人在connected信号里马上调readCharacteristic结果就是什么都读不到。另外一个真实体会Android上有些外设会保持GATT连接但长时间不通信几秒钟后系统可能因为空闲超时主动断掉。想让连接更稳定你可以在connectionUpdated信号里检查连接间隔必要时通过requestConnectionParameters调整参数。不过这个API在Qt6中的签名和平台支持程度不一建议先确认目标平台是否支持。3.3 服务与特征发现流程GATT连接建立后接下来就是发现服务和特征。这是调试助手的核心工作流很多新手卡在这一步。void BleManager::onConnected() { connect(m_controller, QLowEnergyController::serviceDiscovered, this, BleManager::onServiceDiscovered); connect(m_controller, QLowEnergyController::discoveryFinished, this, BleManager::onDiscoveryFinished); m_controller-discoverServices(); } void BleManager::onServiceDiscovered(const QBluetoothUuid serviceUuid) { // 把服务Uuid加入列表 m_serviceList.append(serviceUuid); }服务发现完成后拿到的是服务UUID列表。要读取服务下的特征还需要为每个服务创建QLowEnergyService对象再执行discoverDetails()。QLowEnergyService *service m_controller-createServiceObject(uuid, this); connect(service, QLowEnergyService::stateChanged, this, BleManager::onServiceStateChanged); connect(service, QLowEnergyService::characteristicChanged, this, BleManager::onCharacteristicChanged); connect(service, QLowEnergyService::characteristicRead, this, BleManager::onCharacteristicRead); connect(service, QLowEnergyService::characteristicWritten, this, BleManager::onCharacteristicWritten); service-discoverDetails();为什么服务发现这么关键因为BLE设备的属性模型是树状的设备 - 服务 - 特征 - 描述符。你在调试时经常要一层层点进去查看这和你在LightBlue里看到的层级是完全一致的。如果discoverDetails()执行完stateChanged没有得到QLowEnergyService::ServiceDiscovered状态就说明这一步失败了可能的原因包括设备端服务缓存、连接在发现过程中断、或者外设本身不支持标准GATT发现服务。经验是stateChanged里一定要判断状态是否为ServiceDiscovered再操作特征否则会碰到一堆中间态。3.4 特征读写与通知订阅和硬件交互的关键特征Characteristic是BLE通信的最小操作单元。调试助手里最常用的三类操作就是读、写、订阅通知Notify/Indicate。读操作很简单service-readCharacteristic(characteristic);对应信号characteristicRead会携带读取到的值在回调里转成十六进制显示即可。写操作的坑多一点。首先要分清WriteWithResponse和WriteWithoutResponse需要确保外设收到数据时用带响应的写WriteWithResponse对实时性要求高、数据量大、不需要确认时用无响应写WriteWithoutResponse但写的速度太快会溢出外设缓冲区导致丢包。// 带响应写入 service-writeCharacteristic(characteristic, data, QLowEnergyService::WriteWithResponse); // 无响应写入适合大数据流 service-writeCharacteristic(characteristic, data, QLowEnergyService::WriteWithoutResponse);如果你要测试长包数据比如MTU 247字节用无响应写配合合适的间隔约10ms~20ms会稳很多。通知订阅是调试助手里最实用的功能因为很多外设的数据都是主动上报的比如心率、温湿度、电量。订阅本质上就是往特征对应的Client Characteristic Configuration描述符UUID 0x2902里写入使能值const QLowEnergyDescriptor notifDesc characteristic.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); if (notifDesc.isValid()) { // 0x0001 开启Notify0x0002 开启Indicate service-writeDescriptor(notifDesc, QByteArray::fromHex(0100)); }写好之后外设上报的数据就会通过characteristicChanged信号返回。很多新手订阅不成功最常见原因就是没等discoverDetails()完成就去写描述符写完自然没反应。另外还要看特征的属性位如果特征的Properties不包含Notify或Indicate写描述符也会被外设拒绝。查看属性位可以用characteristic.properties() QLowEnergyCharacteristic::Notify判断最好在界面上把每个特征的属性显示出来方便排查。3.5 MTU调整与大数据包收发BLE经典模式下单个ATT包是23字节3字节头 20字节有效载荷这对调试点亮电量之类的场景够用但你要是传固件升级包、传感器波形数据20字节一包就很痛苦了。这时候需要协商MTU。在Connection Event中中央设备可以主动发起MTU协商。Qt的接口是if (m_controller-mtu() 247) { m_controller-requestMtu(247); }注意几点requestMtu要在连接成功、服务发现完成之后调用别在connected信号里立刻调部分安卓系统在服务发现期间再改MTU会出问题。Android系统对MTU的最大值有限制一般而言requestMtu(247)在Android 5.0有效但有些外设的ATT MTU上限可能只有23或更小协商返回值可能小于请求值。协商完成后每次writeCharacteristic一次能带的字节数可以到MTU-3字节。如果你的数据超过这个长度必须自己分包在业务层加帧头、序号、长度接收端重组。QtBluetooth不会帮你做这个拆包逻辑。如果你要在界面显示当前MTU可以在connectionUpdated或协商完成的信号里读取qDebug() MTU: m_controller-mtu();调试助手如果有这个显示对你排查大包发送失败会有很大帮助。4.本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门