ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Qt蓝牙BLE设备扫描实战:从权限配置到API详解

2026/9/1 6:43:21 拓冰建站 浏览量
Qt蓝牙BLE设备扫描实战:从权限配置到API详解 简介面向Qt开发者的蓝牙通信实现资料包以QtBluetooth库为核心配套可直接运行的Bluetooth实例工程和Serial_Net_Bluetooth_Debug_Assistant_3.1.1.1.zip调试助手覆盖设备搜索、适配器设置、BLE与经典蓝牙连接及数据收发等完整流程。资源共12个文件以.cpp源文件、.h头文件、.ui界面文件及.pro工程文件为主体整体约17.84MB目录结构清晰便于按模块对照学习。实例工程包含mainwindow、chatclient、servicediscoverydialog等模块分别对应主界面、客户端通信和设备发现服务支持全局与局部设备搜索、蓝牙适配器开关与可见性设置、BLE和常规蓝牙设备连接以及实时收发通讯。该调试助手既可用于开发阶段的功能验证也可作为日常蓝牙设备调试的辅助工具帮助开发者快速定位连接异常或数据收发错误适合有基础C/Qt知识、需要快速集成蓝牙功能的开发者以及网络调试、可穿戴设备、智能家居等典型应用场景。目前已有224人学习浏览整体文件构成兼顾源码阅读与工程演示适合直接导入Qt环境运行体验可作为蓝牙应用开发的入门与实战参考。 做设备通讯的上位机总有绕不开的“无线化”需求。我之前接过一个数据采集的项目现场设备分布在车间不同位置拉串口线不现实WiFi又太费电最后选了蓝牙BLE这条路。Qt的QtBluetooth模块从扫描、配对到读写特征值整套链路走下来踩了一堆文档里没写明白的坑。这个系列就是想把这些过程完整记录下来第一篇文章先聚焦最前面的“设备扫描发现”环节它是整个蓝牙通讯的地基地基没打好后面连接、收发数据全都无从谈起。这篇文章适合两类人一是刚接触QtBluetooth、想快速跑通设备扫描并搞清楚API用法的开发者二是已经在做蓝牙上位机、但被各种隐藏问题卡住的老手可以对照排查。文章里会涉及Qt版本选择、Android和Windows的权限差异、核心类的用法以及我在真机上实际遇到的几个典型问题。1. 先搞清楚这个项目要干的事1.1 蓝牙设备通讯的完整链路很多人一上来就搜“Qt 蓝牙通讯代码”然后直接跳到连接和读写结果后面被各种莫名其妙的报错搞得晕头转向。原因很简单蓝牙通讯不是“连上就能发数据”那么一步它是一条比较长但逻辑清晰的链路扫描发现设备拿到周边蓝牙设备的地址、名称、信号强度(RSSI)等基本信息连接设备根据第一步拿到的地址发起连接经典蓝牙还会先走配对流程服务发现连接成功后在设备暴露的GATT服务里找到你关心的那个服务(Service)和特征(Characteristic)特征值读写与通知订阅对选中的特征值执行读写操作或者订阅Notify/Indicate实现真正的数据交互。这个项目整体要做的事就是把这四步串起来做成一个小巧的设备通讯调试工具。界面不需要复杂一个设备列表、一个连接区域、一个数据收发窗口就够了核心难点其实全在链路本身的处理上。1.2 为什么第一篇文章只讲扫描我在规划这个系列的时候特意把“扫描”单独拆成一篇。因为它在整个链路里承担的角色太关键了没有准确的设备扫描后面连谁都连不上。而且扫描阶段遇到的问题最多权限配置、协议选择、设备不可见、后台扫描限制全扎堆在这一步。另一个原因是扫描这块的API形态比较独立可以单独跑通验证。QBluetoothDeviceDiscoveryAgent这一个类配合QBluetoothDeviceInfo就能把设备和信号完整地呈现出来。你先把这个环节跑明白后面的连接、服务发现才算有落点。所以这篇博文的实践目标很明确打开App就能扫出可连接设备并在界面上看到名称、地址和信号强度。2. 选型BLE和经典蓝牙选哪个2.1 两种协议的本质区别蓝牙发展到今天实际应用里就是两大分支经典蓝牙(BR/EDR)和低功耗蓝牙(BLE)。选错方向后面的开发难度会成倍上升。我把它们的核心差异整理了一张表对比维度低功耗蓝牙(BLE)经典蓝牙(BR/EDR)功耗极低纽扣电池可跑数月高适合持续传输场景传输速率实际应用通常在几十KB/s级别适合小数据高满足音频、文件传输连接建立毫秒级秒级配对流程更长典型设备传感器、温控器、心率带、数据采集模块蓝牙耳机、音箱、文件传输QtBluetooth支持度GATT API支持较完善SPP支持非常有限坑很多开发复杂度概念多Service/Characteristic但流程固定配对麻烦DataTransfer API不直观实际项目里怎么选我的经验是凡是“设备端是传感器/采集模块/控制器”这种场景优先考虑BLE。比如温湿度采集器、PLC的状态模块、扫码枪基本都是BLE。原因也不难理解这类设备讲究低功耗、长待机、小数据量交互BLE物理层设计就是为这个准备的。而经典蓝牙的音频和高速文件优势在设备通讯领域并不重要。2.2 QtBluetooth对两种协议的支持情况QtBluetooth这个模块从Qt 5.x开始就比较稳定了但“稳定”也分方向。在BLE生态上它提供了QBluetoothDeviceDiscoveryAgent、QLowEnergyController、QLowEnergyService、QLowEnergyCharacteristic这一整套GATT链路流程完整、文档相对清晰。在经典蓝牙这块主要是QBluetoothSocket配合RFCOMM协议但SPP串口模拟协议的支持就非常尴尬很多情况下你得自己去搞服务发现和UUID匹配资料少、各平台行为还不一致。举个真实例子有个朋友用某品牌的蓝牙串口透传模块模块默认走SPP。他在Windows上调试还算顺利一到Android上就各种连不上。后来查半天发现是Qt版本对经典蓝牙的服务发现实现存在兼容问题。最后他换了个支持BLE的透传模块代码重写一遍反而顺利跑通了。所以如果需求允许尽量让硬件配合选BLE开发效率会高很多。3. 动手前先把环境跑通3.1 Qt版本选择与模块配置我在这个项目里用的是Qt 5.15.2。虽然Qt 6已经出来很久了但5.15.2在工业/设备类项目里还是占有率最高的LTS版本之一坑也基本被踩干净了。Qt 6的蓝牙API大体一致但底层依赖和权限处理有差异如果参考这篇文章用的Qt 6稍微注意一下Android权限API的变化就行。在项目文件(.pro)里首先要加上bluetooth模块QT core gui bluetooth greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET BtDeviceDemo TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp HEADERS \ mainwindow.h这一步很简单但容易漏。如果忘了加QT bluetooth编译时会出现一堆“QBluetoothDeviceDiscoveryAgent: No such file”之类的头文件找不到错误很多人卡在第一步就是这个原因。3.2 Android平台的权限配置如果你和我一样最终目标是在Android设备上跑大多数手持机、PDA都是Android系统权限配置是扫描阶段最绕不开的一关。Android的蓝牙权限经历过多轮变化很多人用旧教程的写法在Android 12以上的设备上会直接扫描空白。先看pro文件里的权限声明android { ANDROID_PERMISSIONS \ android.permission.BLUETOOTH \ android.permission.BLUETOOTH_ADMIN \ android.permission.ACCESS_FINE_LOCATION }这里有个关键点Android 6.0API 23开始蓝牙扫描不仅需要蓝牙权限还需要定位权限。因为系统认为扫描蓝牙设备可以间接推断用户位置所以ACCESS_FINE_LOCATION是必须的。我在真机上调试时第一次就漏了这个结果扫描结果一直是空的还以为是代码问题排查了好久才发现是权限没给全。如果你的目标设备是Android 12API 31以上还需要额外的三个权限BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE。Qt 5.15.2的默认AndroidManifest模板里并没有自动带上这些权限需要手动修改AndroidManifest.xmluses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE /权限光是声明还不够Android 6.0以上的运行时权限必须在代码里动态请求。Qt 5.15里最直接的方式是用QtAndroid命名空间#if defined(Q_OS_ANDROID) #include QtAndroid #include QAndroidJniObject bool requestAndroidPermissions() { QStringList permissions; // Android 12 需要 BLUETOOTH_SCAN低版本需要 FINE_LOCATION // 统一请求系统会自动忽略不支持的权限 permissions android.permission.BLUETOOTH_SCAN android.permission.BLUETOOTH_CONNECT android.permission.ACCESS_FINE_LOCATION; auto result QtAndroid::requestPermissionsSync(permissions); for (const auto entry : result) { if (entry.second ! QtAndroid::PermissionResult::Granted) { qWarning() 权限被拒绝: entry.first; return false; } } return true; } #endif如果是Qt 6可以用QPermission相关的API思路一样类名变了而已。我在实际项目里会把权限请求放在main函数启动时先执行在弹窗还没出现前程序什么都不做避免用户还没点允许App就已经开始扫描。3.3 Windows和Linux开发的注意点做这类型项目经常是Windows上开发调试然后交叉编译到Linux开发板或者Android真机。Windows上的Qt蓝牙底层走的是WinRT接口所以要求Windows 10及以上系统。如果你还在用Windows 7那就基本告别QtBluetooth了不是代码问题是系统底层不支持。Linux环境下需要系统里有BlueZ协议栈和对应的开发包。编译前记着装依赖sudo apt-get install libbluetooth-dev bluez另外如果在Linux开发板上运行Qt程序时遇到类似“qxcbconnection: failed to initialize xrandr”这样的报错多半是图形平台插件的问题跟蓝牙无关。可以尝试设置QT_QPA_PLATFORMoffscreen跑无界面测试或者把xcb相关的系统库补齐。这类问题我在调试蓝牙逻辑时遇到过虽然不影响蓝牙功能本身但会卡住App的启动流程。4. 核心API与扫描代码实操4.1 本地蓝牙适配器的状态检查开始扫描前先检查设备蓝牙开关状态是个好习惯不然用户蓝牙都没开你扫半天也扫不出东西体验很差。QBluetoothLocalDevice就是干这个的#include QBluetoothLocalDevice void MainWindow::checkBluetoothState() { QBluetoothLocalDevice localDevice; if (!localDevice.isValid()) { ui-statusLabel-setText(当前设备不支持蓝牙); return; } QBluetoothLocalDevice::PowerState state localDevice.powerState(); if (state QBluetoothLocalDevice::PowerOff) { localDevice.powerOn(); // 尝试请求开启蓝牙 qDebug() 请求开启蓝牙; } qDebug() 蓝牙地址: localDevice.address().toString(); qDebug() 蓝牙名称: localDevice.name(); }蓝牙名称和地址会出现在扫描结果里吗不会那是本机的信息但打出来可以快速确认蓝牙适配器有没有被正确识别。在Android上QBluetoothLocalDevice还能用来设置设备的可见性discoverable方便别人来连接你——不过我们的项目是上位机去连别的设备这个功能一般用不上。4.2 设备扫描代理的关键APIQBluetoothDeviceDiscoveryAgent是扫描的核心类一旦用熟它扫描这件事就掌握了一大半。先看几个关键的点start(DiscoveryMethod)启动扫描。DiscoveryMethod有两个枚举值ClassicMethod经典蓝牙扫描、LowEnergyMethodBLE扫描。可以传组合值同时扫描但我实际测试下来同时扫两种模式的稳定性不如分开扫所以建议根据需求单选。setLowEnergyDiscoveryTimeout(int)设置BLE扫描的超时时间毫秒。如果不设置默认是0表示一直扫下去不会自动结束。这在现场调试时挺费电的建议手动设个值。deviceDiscovered信号每发现一个设备触发一次携带QBluetoothDeviceInfo参数。这个信号是你填充设备列表的主要入口。finished/canceled信号扫描结束或手动取消时触发适合在这里收尾比如把“扫描中”状态改回来。errorOccurred信号扫描出错时触发。注意版本差异后面有单独一节讲。QBluetoothDeviceInfo这个类也很重要里面常用的有address()设备MAC地址跨平台可能显示格式略有差异name()设备广播名称很多传感器没配名字会显示unknownrssi()信号强度负值越大代表信号越好比如-40比-70好coreConfigurations()判断设备是BLE还是经典蓝牙serviceUuids()设备广播时携带的服务UUID可以对设备类型做预判4.3 完整扫描代码我在这个demo里用一个QList存储扫描结果因为界面列表要展示列表数据源用QStandardItemModel来驱动实时更新设备名称、地址和信号值。完整的扫描实现如下// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QBluetoothDeviceDiscoveryAgent #include QBluetoothDeviceInfo QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void onStartScan(); void onStopScan(); void addDevice(const QBluetoothDeviceInfo info); void scanFinished(); void scanError(QBluetoothDeviceDiscoveryAgent::Error error); private: Ui::MainWindow *ui; QBluetoothDeviceDiscoveryAgent *m_discoveryAgent; QListQBluetoothDeviceInfo m_deviceList; bool m_scanning false; }; #endif // MAINWINDOW_H// mainwindow.cpp #include mainwindow.h #include ui_mainwindow.h #include QStandardItemModel #include QDebug MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_discoveryAgent(nullptr) { ui-setupUi(this); connect(ui-scanButton, QPushButton::clicked, this, MainWindow::onStartScan); connect(ui-stopButton, QPushButton::clicked, this, MainWindow::onStopScan); } MainWindow::~MainWindow() { delete ui; } void MainWindow::onStartScan() { if (m_scanning) { return; } m_deviceList.clear(); // 如果列表是QStandardItemModel可以在这里clear一下 // ui-deviceList-model()-removeRows(0, ui-deviceList-model()-rowCount()); m_discoveryAgent new QBluetoothDeviceDiscoveryAgent(this); // 信号槽连接 connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, MainWindow::addDevice); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::finished, this, MainWindow::scanFinished); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::canceled, this, MainWindow::scanFinished); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::errorOccurred, this, MainWindow::scanError); // 只扫BLE设备扫完10秒自动结束 m_discoveryAgent-setLowEnergyDiscoveryTimeout(10000); m_discoveryAgent-start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); m_scanning true; ui-statusLabel-setText(正在扫描BLE设备...); qDebug() 开始扫描; } void MainWindow::addDevice(const QBluetoothDeviceInfo info) { // 简单过滤只显示BLE设备 if (!(info.coreConfigurations() QBluetoothDeviceInfo::LowEnergyCoreConfiguration)) { return; } // 避免重复添加 for (const QBluetoothDeviceInfo existing : m_deviceList) { if (existing.address() info.address()) { return; } } m_deviceList.append(info); QString name info.name().isEmpty() ? (unknown) : info.name(); qDebug() 发现设备: name 地址: info.address().toString() RSSI: info.rssi(); // 这里把设备信息填充到UI列表比如QListWidget/QTableView // 我用的是QListWidget简单直观 QString itemText QString(%1 [%2] RSSI: %3 dBm) .arg(name, info.address().toString()) .arg(info.rssi()); ui-deviceList-addItem(itemText); } void MainWindow::scanFinished() { m_scanning false; ui-statusLabel-setText(QString(扫描结束共发现%1个设备).arg(m_deviceList.size())); qDebug() 扫描结束设备数量: m_deviceList.size(); } void MainWindow::scanError(QBluetoothDeviceDiscoveryAgent::Error error) { m_scanning false; switch (error) { case QBluetoothDeviceDiscoveryAgent::PoweredOffError: ui-statusLabel-setText(错误蓝牙未开启); break; case QBluetoothDeviceDiscoveryAgent::InputOutputError: ui-statusLabel-setText(错误蓝牙硬件读写失败); break; case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError: ui-statusLabel-setText(错误没有可用的蓝牙适配器); break; default: ui-statusLabel-setText(错误未知扫描错误); break; } qWarning() 扫描出错代码: error; }这里面有两个容易被忽略的细节。第一个是重复设备过滤扫描过程中同一个设备可能被多次上报尤其是BLE设备广播包不断重发不按地址去重的话列表会疯狂刷屏。第二个是扫描结束后再次扫描QBluetoothDeviceDiscoveryAgent不能重复使用每次刷新设备列表必须new一个新实例不然会触发“agent already active”之类的错误。这也是我在代码里每次都在onStartScan里new的原因。RSSI值的展示也是个实用功能。现场调试时拿着手机和设备边走边看RSSI数值越大代表离得越近可以快速锁定设备位置。有些项目还需要根据信号强度自动选择连接目标这个值就直接能派上用场。扫描过程中还有个常见需求是“停止扫描”。调用m_discoveryAgent-stop()就可以之后会触发canceled信号代码里我已经把canceled和finished都接到了scanFinished上无论正常结束还是手动停止状态都能一致收尾。5. 扫描阶段常见问题与排查记录5.1 扫描不到设备的几个方向扫描为空是碰到最多的一个现象。如果代码没问题、设备也在旁边要考虑以下几个因素我整理成了一张速查表现象可能原因排查方向扫描结果完全为空Android运行时权限没给检查定位权限/蓝牙权限有没有弹窗、是否拒绝扫描结果完全为空目标设备不可被发现确认设备处于广播/可发现模式部分传感器要按键触发广播结果为空但不出错Android 12没加BLUETOOTH_SCAN权限看AndroidManifest.xml是否补充新权限只扫到部分设备扫描模式选错了ClassicMethod和LowEnergyMethod要分开试列出设备但全显示unknown设备广播包不带完整名称这不影响连接用地址定位即可Linux上扫描不出设备BlueZ服务未运行/权限不够执行sudo service bluetooth start确认用户有权限尤其是“设备不可被发现”这条很多做传感器硬件的人熟悉但做App的可能想不到。很多BLE从设备有省电策略平时不广播按一下按键才进入可连接广播状态。如果目标设备在扫描列表里看不到先确认它有没有真的在广播。5.2 Android权限引发的坑Android的权限是扫描环节的“头号杀手”。我有个实际案例同一套代码在Android 9的PDA上跑得好好的换到Android 13的手机上扫描结果直接为空连log都不报错。后来对比发现Android 12以上的系统要求使用BLUETOOTH_SCAN权限旧代码只申请了BLUETOOTH和ACCESS_FINE_LOCATION。还有个容易踩的坑是运行时权限和清单权限都要有。哪怕你在AndroidManifest.xml里声明了权限Android 6.0以上的系统运行时还是要动态请求一次。Qt的QtAndroid::requestPermissionsSync可以同步请求但在UI里主线程同步请求会导致界面短暂卡住如果在意体验可以用requestPermissions异步版本或者干脆在启动画面阶段就请求完。我个人的建议是在main函数里先请求权限不通过就直接退出或提示用户去设置里开。因为App的核心功能就是蓝牙没权限整个应用没意义。而且提前请求比用户点了扫描按钮之后再弹窗要自然得多。5.3 Qt版本差异与信号兼容问题Qt自身版本迭代也留了不少坑。最典型的是error信号的变化Qt 5.12之前QBluetoothDeviceDiscoveryAgent的错误信号叫error(QBluetoothDeviceDiscoveryAgent::Error)Qt 5.12开始新增了errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)同时保留了旧的error以便兼容。但旧信号和QObject::error属性的名字冲突很容易在connect时报错。新代码统一用errorOccurred就好。还有一个我在Qt 5.15.2上遇到的细节BLE扫描超时setLowEnergyDiscoveryTimeout设置后的行为表现和文档描述有出入。文档说超时值单位为毫秒但5.15.2有些平台上设置太小的值比如1000ms会导致扫描提前结束设备还没扫完就停了。我改成10000ms后稳定多了。如果发现扫描“秒结束”先检查是不是这个值设得太小。不同Qt版本对同一设备的扫描结果也可能不同比如有些老的BLE设备广播包解析不完整在Qt 5.12上显示不出名称换到5.15就没问题。这种兼容性问题很难追我一般会把扫描到的地址、RSSI先打出来至少能确认设备有没有被发现名称问题可以靠连接成功后重新读取设备信息来弥补。扫描这一步跑通之后设备的基础信息列表就有了下一步要做的是点击设备发起连接然后去发现服务(Service)和特征(Characteristic)最后才能读写数据。那部分要用到QLowEnergyController和QLowEnergyService比扫描更繁琐坑也更多。特别是连接状态切换和信号槽的触发时机稍不注意就会出现“数据写进去了但设备没反应”的怪现象。这些内容我放到系列第二篇里详细讲。目前这个扫描demo已经能在我手头的几个BLE传感器模块上稳定跑起来了如果你也正在做类似的功能先把扫描跑通后面会顺很多。本文还有配套的精品资源点击获取