news 2026/9/1 6:55:21

QtBluetooth开发实战:环境配置、权限处理与HC-05设备扫描

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QtBluetooth开发实战:环境配置、权限处理与HC-05设备扫描

简介:一份面向 Qt 开发者的蓝牙通讯实践资源,围绕 QtBluetooth 模块讲解如何实现设备搜索、连接与数据收发,适用于正在学习 Qt 蓝牙编程或需要快速搭建 BLE 与常规蓝牙调试工具的开发者。资源共 12 个文件,含 4 个 cpp 源文件、3 个 h 头文件、2 个 ui 界面文件、pro 工程文件、user 配置文件,以及 1 个蓝牙调试助手压缩包,整体大小约 17.84MB,代码结构清晰,便于阅读与复用。实例功能覆盖全局与局部设备搜索选择、蓝牙适配器设置、BLE 与传统蓝牙设备支持,以及实时收发通讯,并配有 Serial_Net_Bluetooth_Debug_Assistant_3.1.1.1 调试助手,可直接用于联调验证。已有 224 人学习下载,适合需要参考 QtBluetooth API 用法、界面设计与蓝牙调试流程的中高级 Qt 开发者。 上个月我把一块HC-05蓝牙串口模块接到单片机板上,想用电脑做上位机工具,通过蓝牙远程看串口日志。评估了一圈之后决定用QT,准确说是用QtBluetooth这套自带蓝牙库来做设备通讯。折腾下来发现坑不少:有的坑在权限声明,有的坑在平台差异,有的坑在类与类的调用顺序。这篇是这个系列的第一篇,先把QtBluetooth的整体边界、环境准备、权限配置、四大核心类,以及最基础的设备扫描Demo讲清楚,顺便把HC-05和CSR芯片这类的经典蓝牙设备识别技巧一并分享。不管你是要连HC-05模块,还是想给现有Qt程序加上蓝牙双向数据传输能力,这篇文章的代码和思路可以直接抄作业。

1. 先想清楚QtBluetooth到底能帮你干什么

1.1 经典蓝牙与低功耗蓝牙:别在一开始就选错方向

QtBluetooth并不是一个万能蓝牙驱动,它是Qt负责应用层蓝牙协议的一套封装,覆盖的范围是经典蓝牙(BR/EDR)和低功耗蓝牙(BLE)。这两类虽然都叫蓝牙,但工作的协议栈、通讯模型、API写法完全不一样,选错方向后面的开发节奏会乱。

  • 经典蓝牙走RFCOMM/SPP,面向的是“建立一个串口一样的数据通道”,适合做数据透传、串口调试、模块控制,HC-05、HC-06这类蓝牙转串口模块就是典型代表。
  • 低功耗蓝牙走GATT,面向的是“服务+特征值”模式,适合做传感器数据上报、遥控、广播设备,很多物联网腕带、Beacon、健身设备都是这种。

因为这篇文章的目标是设备通讯,而且用的是HC-05这种串口透传模块,所以我全文都围绕经典蓝牙的RFCOMM/SPP协议展开。如果你要做BLE项目,类名相似但流程不同,后续我可以单独写一篇,别混着看。

1.2 QtBluetooth只负责应用层,别指望它驱动适配器

很多新手容易有个误解:装上Qt就能直接搜蓝牙,搜不到就怀疑库坏了。实际上QtBluetooth在Windows调用本机蓝牙协议栈,在Linux调用BlueZ,在Android调用系统的蓝牙框架。它做的事情是把你调用的扫描、连接、收发数据翻译成各平台能理解的指令,底层驱动和固件你还是得交给操作系统。

这意味着两件事:第一,系统蓝牙必须是通的,设备管理器里没有蓝牙或驱动有黄色感叹号,Qt再怎么调也没用;第二,各平台行为会有差异,比如某些Windows适配器搜不到已隐藏的设备,Linux下BlueZ版本过低会缺API,Android不同版本对权限的处理逻辑又不一样。搞清楚这个边界,你排查问题的时候至少不会一头扎进代码里去猜。

2. 环境准备:版本选型与三端实测差异

2.1 版本选型:我建议一定从5.15起步

Qt的版本选择对蓝牙开发影响很大。我的建议很直接:如果项目没有非用Qt 6不可的理由,先用5.15.x。我实际用的是5.15.2,这套API在Windows和Android上都很稳定,网上能搜到的资料也基本都以5.15为基准。

Qt 6的QBluetooth API有调整,比如QBluetoothDeviceInfo::address()在一些版本里被标记为废弃,扫描方式和Android权限接入方式也有变化。不是说Qt 6不能用,而是你踩到一个坑时,可能查到的解决方案还是5.x的写法,或者反过来,对不上会很折腾。先把5.15跑通,再考虑升版本,这是成本最低的路径。

安装时记得勾选对应平台的蓝牙模块,Qt 5里它属于Connectivity模块下的Bluetooth组件。有人只装了msvc或gcc那一套核心组件,编译时找不到<QBluetoothSocket>头文件,其实就是漏勾了。

2.2 Windows、Linux、Android三端实测差异

Windows端是最省心的,只要系统蓝牙正常工作,QtBluetooth能直接走微软蓝牙栈操作经典蓝牙SPP。我遇到的一个典型问题是CSR8510这类USB蓝牙适配器的驱动,设备管理器里显示已识别但搜索不到设备,多半是驱动模式被搞混了。CSR8510在Windows下一般用系统自带驱动即可,如果之前装过第三方蓝牙驱动栈,建议先把驱动清干净再重新识别,不然Qt扫描会一直返回空列表。

Linux端依赖BlueZ。编译前先确认有蓝牙头文件,Debian系一般需要libbluetooth-dev和BlueZ运行环境,缺少的话Qt程序编译时会在头文件阶段报错。如果你在Linux跑带界面的Qt程序还遇到过failed to initialize xrandr,那是X11下的库或权限问题,跟蓝牙本身无关,但在跑蓝牙Demo之前也得先处理掉,否则程序都起不来。

Android端复杂在权限和系统版本差异。Android 11及以下扫描蓝牙需要定位权限,而且很多国内手机上你只开定位开关还不够,会自动定位服务也要开,不开就扫不到设备,这个坑等会儿单独说。Android 12以上引入了BLUETOOTH_SCANBLUETOOTH_CONNECT一组新权限,旧项目直接拿到新系统上跑,经常会因为在运行时没有申请这组权限而静默失败。所以三端里Android在权限上花的时间最多,但一旦配好,跑起来也是最稳的。

3. 权限声明与工程配置:搞不定这步扫描就是空转

3.1 Android权限变化时间线

Android权限是蓝牙开发里最容易被忽略又最致命的一环。我按版本线整理一下,你对照自己的目标设备配置就不会错:

  • Android 6.0到Android 11:扫描蓝牙设备需要位置权限。Manifest里声明ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION,同时运行时动态申请。注意蓝牙扫描在系统层面被归类为获取位置信息,你不开定位授权,系统就认为你无权查看附近的蓝牙设备。
  • Android 12及以上:新增BLUETOOTH_SCANBLUETOOTH_CONNECT,这些是需要动态申请的运行时权限,同时传统的位置权限要求并没有完全消失。如果目标机型是Android 14,又用到某些蓝牙Profile,还可能遇到系统自动禁用某些协议的情况,这时能退而求其次的做法是只申请你真正用到的权限,别一把梭全申请。

Manifest里最小要这样声明:

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

3.2 工程文件里该加什么

无论哪个平台,.pro文件里都要加上:

QT += core gui bluetooth

Windows和Linux基本不需要额外配置,编译时能找到模块就行。Android要找到项目目录下的AndroidManifest.xml,把上面的权限声明合进去,再重新构建APK。实际开发中我建议把权限申请放在扫描按钮按下之前,用QtAndroid工具类处理运行时请求:

#ifdef Q_OS_ANDROID #include <QtAndroid> QtAndroid::requestPermissions( {"android.permission.ACCESS_FINE_LOCATION", "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"}, [](const QtAndroid::PermissionResultMap &result) { // 这里有回调结果,可以根据结果决定是否继续 }); #endif

这个写法在Qt 5.15下可以直接用。跑真机测试时,及时权限弹窗点了允许也不代表系统定位已经开启,我吃过亏,所以再次强调:Android上扫描蓝牙要保证定位总开关处于开启状态,否则Qt的扫描信号一个都不会来。这是整个项目里我踩得最深的一个坑。

4. 核心类拆解:四个类串起一条通讯链路

4.1 QBluetoothLocalDevice:先看本机蓝牙状态

很多人一开始就把QBluetoothDeviceDiscoveryAgent搬出来扫描,却忘了检查本地蓝牙是否打开。QBluetoothLocalDevice就是干这个的。

QBluetoothLocalDevice localDevice; if (localDevice.isValid()) { localDevice.powerOn(); qDebug() << "本机地址:" << localDevice.address().toString(); qDebug() << "状态:" << localDevice.hostMode(); }

hostMode()会返回当前蓝牙模式,常见的是QBluetoothLocalDevice::HostPoweredOff表示蓝牙没开,HostDiscoverable表示可被发现,HostConnectable表示可连接但不可被发现。实际调试时,我习惯把这几项打到日志里,很多扫描不到设备的问题根本原因就是本机蓝牙处于关闭状态,Qt的扫描代理会正常返回“完成”,但一个设备都搜不到。

另外注意一点:QBluetoothLocalDevice的调用依赖Qt事件循环。不要在构造函数里、QCoreApplication::exec()还没进入之前就去做状态判断和扫描,那时候信号槽机制还没完全转起来,你会看到“qcoreapplication::exec() 之后就无法捕获了”这类困惑,其实是时机不对。所有蓝牙动作都放在进入事件循环之后再去触发。

4.2 QBluetoothDeviceDiscoveryAgent:先“发现”再谈其他

发现设备是整个通讯链路的第一环。QBluetoothDeviceDiscoveryAgent负责扫描附近的蓝牙设备,核心信号有两个:deviceDiscovered在每发现一个设备时触发,finished在扫描结束时触发。

QBluetoothDeviceDiscoveryAgent *agent = new QBluetoothDeviceDiscoveryAgent(this); connect(agent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, [](const QBluetoothDeviceInfo &info) { qDebug() << "发现设备:" << info.name() << info.address().toString(); }); connect(agent, &QBluetoothDeviceDiscoveryAgent::finished, this, []() { qDebug() << "扫描结束"; }); agent->start();

start()默认行为在经典蓝牙和低功耗蓝牙上会有差别,你可以在参数里指定QBluetoothDeviceDiscoveryAgent::ClassicMethod只看经典蓝牙,也可以默认跑完后再从QBluetoothDeviceInfocoreConfigurations()里区分设备类型。我建议扫描完成后统一过滤,这样界面逻辑更清晰。

扫描是异步的,别在调用start()之后立刻去查设备列表,那一定还是空的。正确做法是把扫描结果先收集下来,等finished信号到了再统一刷新UI或处理逻辑。

4.3 QBluetoothServiceDiscoveryAgent:找到设备之后还要找到“门牌号”

设备发现只是找到了对方硬件,能不能连上,还得看对方有没有开放你要的服务。经典蓝牙SPP设备通常会暴露一个串口服务,这个服务的UUID一般是00001101-0000-1000-8000-00805F9B34FB

QBluetoothServiceDiscoveryAgent可以查询设备上到底提供哪些服务:

QBluetoothServiceDiscoveryAgent *serviceAgent = new QBluetoothServiceDiscoveryAgent(this); serviceAgent->setRemoteAddress(deviceAddress); connect(serviceAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this, [](const QBluetoothServiceInfo &info) { qDebug() << "服务名:" << info.serviceName(); qDebug() << "服务UUID:" << info.serviceUuid().toString(); }); serviceAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);

这个类在实际开发里容易被跳过,因为像HC-05这类模块通常固定走RFCOMM通道1,你直接指定通道也能连上。但如果你想做一个通用工具,能够连接不同厂家的SPP模块,服务发现这一步就不能省,不同模块的RFCOMM通道可能不一样,靠扫描出来的服务信息去连接才是通用方案。

4.4 QBluetoothSocket:真正的数据通道

前面这些类都在“找”,真正承担数据收发的其实是QBluetoothSocket。它跟QTcpSocket的用法非常像,底层走RFCOMM协议。

QBluetoothSocket *socket = new QBluetoothSocket( QBluetoothServiceInfo::RfcommProtocol, this); socket->connectToService(QBluetoothAddress("00:13:EF:00:00:00"), 1); connect(socket, &QBluetoothSocket::readyRead, this, [this]() { QByteArray data = socket->readAll(); qDebug() << "收到数据:" << data; }); // 发送数据 socket->write("AT\r\n");

构造函数第一个参数指定协议类型,经典蓝牙SPP用QBluetoothServiceInfo::RfcommProtocolconnectToService的参数是目标蓝牙地址和RFCOMM通道号,这个通道号可以从服务发现结果里拿到,也可以用像HC-05这类默认模块约定好的通道1。

QBluetoothSocketstateChanged信号可以帮你定位连接卡在哪一步,UnconnectedStateConnectingState再到ConnectedState,如果卡在ConnectingState一直没有后续,大概率是对端没有开放对应通道或者没有配对过。收数据用readyRead,发送直接用write,和常见的Qt socket编程习惯一致,上手成本很低。

到这一步只是把链路串起来的粗略示意,服务发现和socket的完整交互逻辑我会在系列下一篇里展开讲,这一篇重点是让你知道每个类负责哪一环,不至于看代码时一头雾水。

5. 第一个Demo:把附近蓝牙设备全部扫出来

5.1 可直接跑的扫描代码

这篇既然定位在“设备通讯(1)”,那第一个能拿到的成果就是把附近蓝牙设备完整扫出来。我贴一段自己项目里的精简版,头文件和实现都放一起,方便你直接建一个Qt Widgets工程复制进去跑。

// MainWindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QListWidget> #include <QPushButton> #include <QBluetoothDeviceDiscoveryAgent> #include <QBluetoothDeviceInfo> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); private slots: void startScan(); private: void onDeviceDiscovered(const QBluetoothDeviceInfo &info); void onScanFinished(); QPushButton *scanButton; QListWidget *deviceList; QBluetoothDeviceDiscoveryAgent *discoveryAgent; }; #endif // MAINWINDOW_H
// MainWindow.cpp #include "MainWindow.h" #include <QDebug> #include <QBluetoothLocalDevice> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { scanButton = new QPushButton(tr("扫描蓝牙设备"), this); deviceList = new QListWidget(this); QWidget *central = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(central); layout->addWidget(scanButton); layout->addWidget(deviceList); setCentralWidget(central); discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); connect(scanButton, &QPushButton::clicked, this, &MainWindow::startScan); connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &MainWindow::onDeviceDiscovered); connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &MainWindow::onScanFinished); // 检查本机蓝牙是否可用 QBluetoothLocalDevice localDevice; if (!localDevice.isValid()) { scanButton->setEnabled(false); qWarning() << "本机蓝牙不可用"; } } void MainWindow::startScan() { deviceList->clear(); scanButton->setEnabled(false); scanButton->setText(tr("扫描中...")); discoveryAgent->start(); } void MainWindow::onDeviceDiscovered(const QBluetoothDeviceInfo &info) { QString displayText = QString("%1 %2") .arg(info.name().isEmpty() ? QStringLiteral("未知设备") : info.name()) .arg(info.address().toString()); deviceList->addItem(displayText); if (info.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) { qDebug() << "经典蓝牙设备:" << info.name() << info.address().toString(); } } void MainWindow::onScanFinished() { scanButton->setEnabled(true); scanButton->setText(tr("扫描蓝牙设备")); qDebug() << "扫描完成,共发现" << deviceList->count() << "个设备"; }

这个Demo跑起来后,点一下按钮,附近在广播的蓝牙设备会陆续出现在列表里。扫描是逐步返回结果的,不是一次性全部到齐,所以列表会一直往上加,等按钮恢复可点状态时表示扫完了。

5.2 识别设备的小技巧:12位蓝牙地址怎么看

蓝牙设备地址看起来是一串十二位十六进制字符,比如00:13:EF:00:00:00,实际是48位二进制地址,分成三段理解会有用:

  • NAP:前16位,即前两组十六进制,比如00:13,这部分不太参与设备寻址逻辑。
  • UAP:中间8位,比如EF,是高地址部分。
  • LAP:后24位,也就是后三组,才是设备身份里最常变化的部分。

这个规则对普通用户的意义在于:你看到地址前缀就能大概判断芯片厂商。00:13:EF这个OUI对应CSR公司的芯片,很多国产HC-05、HC-06模块用的就是CSR方案,因此扫到这种前缀的地址,基本可以断定是这类串口透传模块。这个技巧在设备没有名称或者名称乱码时非常管用,至少能帮你缩小识别范围。

另外,如果同一个设备被扫出来多次,地址不变但名称可能为空或乱码,这是系统层蓝牙缓存导致的,不要慌,以MAC地址为准去判断设备身份更可靠。

6. 常见坑与排查思路:HC-05搜不到、CSR驱动、权限误杀

6.1 常见故障对照表

我把实操中遇到的问题整理成一张表,你扫不到设备或者连接失败时逐项对照,比自己瞎猜快得多:

现象可能原因处理思路
点击扫描后按钮恢复但列表为空系统蓝牙开关未打开QBluetoothLocalDevice先检测本机状态
Android下完全搜不到设备定位权限未授权,或定位总开关没开动态申请权限,同时设置里打开定位
Windows下CSR8510搜不到设备第三方驱动冲突或驱动异常卸载第三方驱动,恢复系统自带驱动
能搜到HC-05但连不上模块没有进入配对模式或通道不对重启模块,检查PIN码,用服务发现拿正确通道
Linux下编译报蓝牙头文件缺失BlueZ开发库没装安装libbluetooth-dev
扫描期间程序卡死蓝牙操作放到了无事件循环的线程确保扫描和socket操作在事件循环内执行

6.2 一次现场排查的完整思路

有次在客户那边调试,Windows笔记本配CSR8510适配器,Qt程序怎么扫都看不到任何设备,但系统自带的蓝牙设置里能看到已配对设备。当时我没有直接改代码,而是按下面这个链路逐步排查:

第一步,先确认设备管理器里蓝牙适配器是否正常,结果看到那里有一个未知设备和两个黄色感叹号,基本锁定是驱动问题。

第二步,卸载之前安装的第三方蓝牙驱动,重启系统,让Windows重新识别CSR8510,设备管理器恢复成正常的“蓝牙”节点。

第三步,回到Qt程序里再扫描,设备就出现了。这说明大多数扫描失败都不是代码问题,而是系统蓝牙这一层没有就绪。

遇到类似问题千万不要一上来就改代码逻辑,先看系统蓝牙是否正常、权限是否到位、设备是否真的在广播。这三个基础问题排除掉,再考虑代码层面的信号连接和调用时机。这也是我强烈建议你在程序里加本机蓝牙状态检测的原因,它能在早期把很蠢却致命的问题直接暴露出来。

另外还有一个容易被忽略的点:有的蓝牙设备在一段时间没有通讯后会自动休眠,这是模块侧的行为,跟Qt无关。HC-05如果一直连不上,可以重新给模块上电,按住模块上的按键再上电会进入AT模式,此时连电脑会识别出不同的服务信息。这不算Qt的问题,但排查链路里一定要有这一步,不然你会怀疑是不是代码写错了。

扫描这部分就先讲到这。下一篇我会把服务发现、RFCOMM通道匹配和QBluetoothSocket的完整收发流程串起来写,到时候你拿这篇的扫描结果直接对接连接逻辑就行。写这个系列的时候我把所有踩过的坑都按真实场景复现了一遍,最深的体会就是:先确认系统蓝牙是通顺的,再让Qt代码去折腾,顺序反了会被各种假象带偏。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 6:54:16

【计算机毕业设计单片机案例】 射频通信下的病房呼叫硬件终端与移动端管控系统设计 基于 STM32 或 51 单片机的 4 路病人呼叫信号采集报警系统设计(020205)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/1 6:54:07

基于J-Link RTT的嵌入式高效日志系统设计与优化

简介&#xff1a;这款源码包围绕Jlink RTT Viewer的日志优化而设计&#xff0c;面向使用ARM Cortex-M系列芯片的嵌入式开发者&#xff0c;旨在解决调试过程中日志缺乏时间标记、优先级不可视、中文乱码等常见问题。工程基于SEGGER的RTT实时终端库实现&#xff0c;提供了INFO、D…

作者头像 李华
网站建设 2026/9/1 6:51:46

OpenCV+MediaPipe人体姿态检测实战:关键点识别与动作判断源码解析

简介&#xff1a;该代码包是面向Python开发者的OpenCV与MediaPipe实时人体姿态检测工程&#xff0c;适合正在学习计算机视觉&#xff0c;或需要在项目中快速接入人体关键点识别功能的读者。压缩包仅7KB&#xff0c;共5个文件&#xff0c;包含Python主程序、txt依赖清单、Markdo…

作者头像 李华
网站建设 2026/9/1 6:51:15

AHT20温湿度传感器使用

工具准备&#xff1a; STM32F407VGT-DISC开发板&#xff0c;AHT20温湿度传感器模块 CubeIDE&#xff0c;CubeMX&#xff0c;vscode 首先从GitHub上拉取驱动代码 libdriver/aht20: AHT20 full-featured driver library for general-purpose MCU and Linux. 然后在CubeMX中创…

作者头像 李华
网站建设 2026/9/1 6:47:56

A*与JPS算法对比:栅格地图路径规划的MATLAB实现与性能测试

简介&#xff1a;这是一份基于MATLAB的A 与JPS路径规划算法对比测试资源&#xff0c;覆盖1010至100100共6种不同分辨率的栅格地图&#xff0c;面向路径规划初学者、算法优化研究者以及机器人导航基础实验场景。压缩包共含38个文件&#xff0c;其中32个为.m脚本&#xff0c;并包…

作者头像 李华
网站建设 2026/9/1 6:47:05

Chrome v72绿色便携版:含完整运行组件,兼容老系统的实用方案

简介&#xff1a;谷歌浏览器72版绿色便携压缩包&#xff0c;集齐完整运行组件&#xff0c;面向需要兼容老旧网页技术、特定插件环境、自动化测试脚本或离线调试场景的用户。无需安装即可用&#xff0c;解压后双击主程序即可启动&#xff0c;保留该版本特有的页面渲染与脚本执行…

作者头像 李华