恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
QtBluetooth开发实战:环境配置、权限处理与HC-05设备扫描
首页
资讯中心
/
QtBluetooth开发实战:环境配置、权限处理与HC-05设备扫描
QtBluetooth开发实战:环境配置、权限处理与HC-05设备扫描
发布时间:2026/9/1 6:55:25
简介一份面向 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版本过低会缺APIAndroid不同版本对权限的处理逻辑又不一样。搞清楚这个边界你排查问题的时候至少不会一头扎进代码里去猜。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_SCAN和BLUETOOTH_CONNECT一组新权限旧项目直接拿到新系统上跑经常会因为在运行时没有申请这组权限而静默失败。所以三端里Android在权限上花的时间最多但一旦配好跑起来也是最稳的。3. 权限声明与工程配置搞不定这步扫描就是空转3.1 Android权限变化时间线Android权限是蓝牙开发里最容易被忽略又最致命的一环。我按版本线整理一下你对照自己的目标设备配置就不会错Android 6.0到Android 11扫描蓝牙设备需要位置权限。Manifest里声明ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION同时运行时动态申请。注意蓝牙扫描在系统层面被归类为获取位置信息你不开定位授权系统就认为你无权查看附近的蓝牙设备。Android 12及以上新增BLUETOOTH_SCAN和BLUETOOTH_CONNECT这些是需要动态申请的运行时权限同时传统的位置权限要求并没有完全消失。如果目标机型是Android 14又用到某些蓝牙Profile还可能遇到系统自动禁用某些协议的情况这时能退而求其次的做法是只申请你真正用到的权限别一把梭全申请。Manifest里最小要这样声明uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /3.2 工程文件里该加什么无论哪个平台.pro文件里都要加上QT core gui bluetoothWindows和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只看经典蓝牙也可以默认跑完后再从QBluetoothDeviceInfo的coreConfigurations()里区分设备类型。我建议扫描完成后统一过滤这样界面逻辑更清晰。扫描是异步的别在调用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::RfcommProtocol。connectToService的参数是目标蓝牙地址和RFCOMM通道号这个通道号可以从服务发现结果里拿到也可以用像HC-05这类默认模块约定好的通道1。QBluetoothSocket的stateChanged信号可以帮你定位连接卡在哪一步UnconnectedState到ConnectingState再到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代码去折腾顺序反了会被各种假象带偏。本文还有配套的精品资源点击获取