
简介面向Qt/C开发者的蓝牙通信实战资源包以QtBluetooth模块为核心演示了全局与局部设备搜索、蓝牙适配器设置、BLE与传统蓝牙连接以及数据收发等完整流程适合需要为桌面或嵌入式应用加入蓝牙功能的开发者参考。包内共12个文件包含4个cpp源代码、3个h头文件、2个ui界面设计文件、1个pro工程文件及辅助配置核心实例Bluetooth可直接编译运行另附Serial_Net_Bluetooth_Debug_Assistant_3.1.1.1.zip蓝牙调试助手支持扫描周围设备、选择连接目标并完成数据收发便于开发者边调试边核对通讯效果。压缩包约17.84MB已有224人学习。通过该实例读者可以掌握Qt蓝牙API的调用方式理解设备发现机制、BLE低功耗连接与串口式收发通讯的实现思路界面设计和逻辑代码相互分离也方便进行二次修改。调试助手本身可作为通用测试工具用于后续蓝牙项目的功能验证与排错。 做上位机开发的朋友应该都有这种体会设备通讯这块串口、TCP、UDP都玩过不少但一提到蓝牙就有点发怵。蓝牙协议栈层数多、设备类型杂、平台差异大过去总感觉是个大坑。实际上Qt官方早就把蓝牙支持封装成了QtBluetooth模块从设备发现、服务扫描到数据收发都有现成接口用起来比想象中省事很多。这个系列打算系统地把QtBluetooth从入门到实战拆一遍今天第一篇重点解决“怎么把周围的蓝牙设备扫出来”以及“工程上怎么配置才不踩坑”适合正在搞Qt上位机、准备接入蓝牙设备的朋友参考。这一篇不涉及BLE连接参数、不涉及数据格式先把架子搭对后面收发数据的时候才省心。1. 项目整体设计与思路拆解1.1 为什么选QtBluetooth而不是虚拟串口或第三方库很多蓝牙透传模块在Windows上会虚拟出一个COM口所以不少老项目直接用QSerialPort去收发。这个方案在单个模块、单一平台下没毛病但问题在于第一它依赖厂商驱动换台电脑或者换到Linux、Android上这套玩法直接就废了第二虚拟串口没法主动发现设备你到底有几台设备在广播、信号强度多少它一概不知道。所以如果你的目标是做一个跨平台的上位机或者你的设备是BLE类的别在虚拟串口这条路上投入太多。QtBluetooth的定位就是用一套统一API覆盖经典蓝牙和低功耗蓝牙。它内部在Windows上走WinRT API在Android上走系统BluetoothAdapter在Linux上走BlueZ的D-Bus接口。对应用层来说只要调用那几十个类的方法就行平台差异被藏在了Qt的插件机制后面。这也是我建议用它做设备通讯底座的核心理由代码只写一遍部署到哪里都相对可控。1.2 QtBluetooth核心类的分工QtBluetooth把协议栈的复杂度拆到了几个类里。用生活化的话说QBluetoothLocalDevice是“我自己的蓝牙开关状态”告诉你的程序本机蓝牙适配器在不在、可不可被发现QBluetoothDeviceDiscoveryAgent是“拿着一台雷达转圈扫”负责发起扫描并持续上报QBluetoothDeviceInfo是“扫到的每一个目标的登记表”记录设备名称、地址、信号强度、设备类型。之后连接阶段还会用到QBluetoothServiceDiscoveryAgent、QBluetoothSocket、QLowEnergyController分别对应服务发现、经典蓝牙RFCOMM通道和BLE控制。但第一篇我们先把前面三个类搞明白就行因为扫描是整个蓝牙通讯流程的入口这一步的数据质量和稳定性直接决定了后面连接模块需不需要反复返工。1.3 这一篇先做什么不做什么这一篇的目标很明确程序启动后点一下按钮把周围可发现的蓝牙设备扫描出来显示在列表里并且通过信号强度初步判断哪些设备是稳定的。理论上你不需要写网络层、不需要手工拼协议只要把Qt的信号槽机制理解透整个流程就通了。至于扫描到之后怎么连接、怎么收发数据留到第二篇。我故意把范围收窄是因为实际项目里很多问题出在“架子没搭对”。你可能会觉得扫描不过是个列表有什么难的但真到现场联调的时候权限、适配器状态、生命周期、平台差异随便一个环节出问题都能让你排查两小时。基础搞扎实比盲目堆功能重要。2. 环境准备与工程配置2.1 Qt版本选择与模块声明qmake/CMake我的开发环境是Qt 6.5.3但下面大部分代码在Qt 5.15.2上也能跑只是个别信号写法有差异我会在对应位置提醒。新建工程之后如果用qmake直接在.pro文件里追加一行QT bluetooth注意这里不一定要和QT core gui合并单独写一行反而更清晰方便后面维护时一眼看出这个工程依赖了哪些模块。如果是CMake工程需要在CMakeLists.txt里显式声明find_package(Qt6 REQUIRED COMPONENTS Bluetooth) target_link_libraries(你的目标名 PRIVATE Qt6::Bluetooth)很多人在这一步卡住编译时报错Project ERROR: Unknown module(s) in QT: bluetooth。十有八九是安装Qt时没有勾选蓝牙模块回到Qt安装器在对应版本下勾选Qt Bluetooth组件补装一遍就行不需要重装整个环境。2.2 不同平台的权限准备蓝牙这东西和串口最大的区别就是权限层级多。在x86 Windows上只要系统蓝牙开着一般代码直接能跑但如果你要部署到Android开发板或者手机App上权限写不全扫描结果就是空的而且还不报错这是最坑的地方。我整理了一张对照表平台必须配置的权限常见坑Windows设置-隐私-蓝牙里允许应用访问Win10 1803及以上部分精简系统蓝牙服务被禁用Android 7~11BLUETOOTH、BLUETOOTH_ADMIN、ACCESS_FINE_LOCATION扫描前必须开启定位否则结果为空Android 12BLUETOOTH_SCAN、BLUETOOTH_CONNECT运行时权限要动态申请不只写ManifestLinuxBlueZ用户加入bluetooth组虚拟机默认没有蓝牙适配器扫描永远为空Android的权限申请除了在AndroidManifest.xml里声明还需要调用运行时权限请求这一点雷区非常大。我见过很多项目Manifest写得很全扫描就是没结果最后发现是Android 12的运行时权限没弹窗确认。如果后面有机会我可以单独写一篇Qt Android运行时权限申请的操作。2.3 程序启动时先检查蓝牙适配器在跑扫描之前先确认本机到底有没有可用的蓝牙适配器。用QBluetoothLocalDevice就能查#include QBluetoothLocalDevice const QListQBluetoothHostInfo adapters QBluetoothLocalDevice::allDevices(); if (adapters.isEmpty()) { qWarning() 本机没有可用蓝牙适配器请先打开蓝牙; return; } for (const QBluetoothHostInfo adapter : adapters) { qInfo() 适配器: adapter.name() 地址: adapter.address().toString(); }这段代码看着简单但能帮你在现场排查时少背很多锅。我遇到过几次“代码没问题就是扫不到”的情况最后发现是客户电脑上蓝牙开关压根没开或者设备管理器里适配器被禁用。程序里加一个启动检查比让用户自己去翻系统设置强得多。3. 核心机制解析扫描为什么是异步的3.1 事件循环与信号槽先说一个容易绕进去的问题蓝牙扫描能不能像串口readLine一样直接阻塞在那里等结果答案是不要这么做。设备扫描往往是秒级的而且操作系统底层会持续回调发现结果如果你在主线程里写一个大循环等结果界面就卡死了用户以为程序崩了。Qt给出的标准解法是信号槽你把扫描请求丢给QBluetoothDeviceDiscoveryAgent然后该干嘛干嘛设备扫到一个回调一个。底层实现里Qt帮你在事件循环中处理了OS的蓝牙回调相当于把你从手写消息队列、线程同步里面解放出来了。这也是为什么你用Qt做蓝牙几乎不需要自己再套一个工作线程来专门等设备——事件循环本身就是消息队列。3.2 设备发现代理完整用法扫描是从创建QBluetoothDeviceDiscoveryAgent开始的。一个需要养成的习惯是把它保存为类成员而不是在槽函数里new一个局部变量。因为扫描是异步的局部变量一个作用域结束就被回收了后面信号来了你连接收对象都没了轻则漏结果重则崩溃。下面这段是初始化m_discoveryAgent new QBluetoothDeviceDiscoveryAgent(this); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, MainWindow::onDeviceDiscovered); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::finished, this, MainWindow::onDiscoveryFinished); connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::errorOccurred, this, MainWindow::onDiscoveryError); m_discoveryAgent-setInquiryType( QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry); m_discoveryAgent-start();setInquiryType里的GeneralUnlimitedInquiry是通用无限查询适合绝大多数场景。个别对耗电敏感的嵌入式设备可以选LimitedInquiry但扫描结果会少很多开发调试阶段我一般不推荐因为你根本不知道周围设备里哪个是你的目标扫描范围窄了很容易漏。3.3 新式语法与Qt5/Qt6差异刚才用的connect写法是Qt5之后推荐的新式语法编译期就能检查信号和槽是否匹配。但QBluetoothDeviceDiscoveryAgent的deviceDiscovered信号在Qt5里参数是QBluetoothDeviceInfo在Qt6里是const QBluetoothDeviceInfo有些老代码用旧式宏写SIGNAL(deviceDiscovered(QBluetoothDeviceInfo))从Qt5升到Qt6就匹配不上了。我的建议是尽量用lambda配合新式语法connect(m_discoveryAgent, QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, [this](const QBluetoothDeviceInfo info) { onDeviceDiscovered(info); });lambda写法还有个好处就是可以直接捕获外部变量做去重、缓存这些临时逻辑不用非把每个逻辑都拆成具名槽函数。当然如果团队规范要求所有槽函数具名那用新式语法连接到成员函数也完全没问题。4. 从扫描到选择完整实操示例4.1 演示窗口与类设计这一节直接把最小可跑的示例串起来。我的演示窗口很简单一个“开始扫描”按钮、一个QListWidget显示结果、一行状态栏显示扫描状态。头文件里的类成员这样声明private slots: void onStartScanClicked(); void onDeviceDiscovered(const QBluetoothDeviceInfo info); void onDiscoveryFinished(); void onDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error); private: QBluetoothDeviceDiscoveryAgent *m_discoveryAgent nullptr; QListQBluetoothDeviceInfo m_devices; QPushButton *m_scanButton nullptr; QListWidget *m_deviceList nullptr;这里我把m_devices单独存了一份目的是后面要排序、去重、传给下一个界面时都有数据源不必每次都从控件的item数据里反查。实际项目里这个列表大概率要传递给服务发现或者连接模块所以一开始就把数据层和界面对应关系设计清楚能省很多重构时间。4.2 核心逻辑一步步讲点开始扫描时先检查状态避免用户连点两次导致重复扫描然后清空上次的结果并启动void MainWindow::onStartScanClicked() { if (m_discoveryAgent-isActive()) { statusBar()-showMessage(正在扫描中请稍候, 3000); return; } m_devices.clear(); m_deviceList-clear(); m_deviceList-addItem(正在扫描...); m_discoveryAgent-start(); }设备发现回调里去重后再把设备名、地址、信号强度拼成一行显示。去重一定要做尤其是外设多的现场可能同一台设备被多个信道重复上报void MainWindow::onDeviceDiscovered(const QBluetoothDeviceInfo info) { for (const QBluetoothDeviceInfo d : m_devices) { if (d.address() info.address()) { return; } } m_devices.append(info); QString text QString(%1 [%2] RSSI:%3 dBm) .arg(info.name().isEmpty() ? QString(未知设备) : info.name()) .arg(info.address().toString()) .arg(info.rssi()); m_deviceList-addItem(text); }扫描结束的槽函数要把“正在扫描...”这个占位项去掉并更新状态栏。这里有个容易被忽略的点finished信号无论是否发现设备都会发所以不要在finished里判断“有没有设备”要看m_devices这个列表为准。4.3 真机测试与效果判读在电脑上跑通之后务必拿手机做一次测试。手机记得把蓝牙设为可被发现有些手机默认“仅已配对设备可见”那扫描结果里就会少一截。测试时注意观察RSSI距离0.5米左右一般-40到-60 dBm隔一堵墙会掉到-70甚至-80。如果看到-90以下说明设备已经处在信号边缘数据收发阶段很容易丢包这种位置做联调意义不大。5. 常见问题与排查技巧实录5.1 扫描问题速查表这些问题我按出现频率排过序前四条占了日常问题里的八成以上。现场联调最怕的就是“不报错但结果不对”这种问题基本都在权限和适配器状态这两个大类里排查顺序也按这个来。现象可能原因排查/解决办法扫描按钮点了没反应本机蓝牙未开启或适配器被禁用用QBluetoothLocalDevice::allDevices()检查一直显示“正在扫描”系统蓝牙栈异常换电脑/开发板交叉验证重启蓝牙服务Android 12扫描为空没有动态申请运行时权限运行时请求BLUETOOTH_SCAN不只写ManifestAndroid 10扫描异常定位权限未开启添加ACCESS_FINE_LOCATION并动态申请Windows能扫到部分设备设备类型不匹配部分BLE设备需要用QLowEnergyController发现列表出现重复设备未按地址去重手动检查已保存的设备地址集合编译报Unknown module bluetoothQt安装缺少蓝牙模块用安装器补装Qt Bluetooth组件agent析构时崩溃扫描未停止就直接销毁先stop()再deleteLater()5.2 实测中踩过的几个坑先说生命周期。QBluetoothDeviceDiscoveryAgent扫描过程中如果窗口被关闭而且agent是局部变量程序轻则信号不触发重则在Windows上直接崩。我在一些demo项目里见过new完忘记delete的写法扫描结束后agent一直悬空第二次点扫描就出问题。建议统一用new QBluetoothDeviceDiscoveryAgent(this)交给Qt父子对象管理窗口销毁时自动释放如果要在不可见对象里长时间扫描记得在finished槽里主动deleteLater。再说地址去重的细节。Windows平台上部分设备返回的address()可能是空字符串或者全零地址只靠address去重会漏设备。我一般用设备地址为主键如果地址为空就退化为用name()rssi()组合判断。这种方式虽然不完美但至少能挡住大部分重复项。还有一个很隐蔽的坑在Linux虚拟机上跑Qt程序系统设置里能看到蓝牙但扫描永远是空。原因很简单虚拟机默认没有把宿主机的蓝牙适配器直通给虚拟机。排查的时候不要纠结代码先看sudo bluetoothctl能不能扫到设备能扫到再回头看Qt层。5.3 扫描结果怎么用一个实用小习惯按我的习惯扫描这个环节我会额外留一个观察项到同一现场连续点三次扫描统计每次结果里有哪些设备反复出现。反复出现的设备大概率信号稳定值得在界面上置顶只出现一次的基本可以判定是旁边工位临时开了一下蓝牙。这个小习惯对后面做自动连接、设备名单管理很有帮助。我自己的做法是把这一篇的工程保存下来把m_devices列表直接透传给下一个页面做服务发现下一篇就从这个入口接着写。先把扫描这关过了后面只是顺水推舟。本文还有配套的精品资源点击获取