低功耗蓝牙概述
Qt Bluetooth 低能耗 API 支持外设/服务器和中心/客户端角色。它支持所有主要 Qt 平台。唯一的例外是 Windows 平台上缺少对外设角色的支持。
什么是低功耗蓝牙
蓝牙低功耗(也称蓝牙智能)是一种无线计算机网络技术,于 2011 年正式推出。它的工作频率与 "传统 "蓝牙相同,都是 2.4 GHz。正如其技术名称所述,其主要区别在于低能耗。它为蓝牙低功耗设备提供了一个机会,使其可以使用纽扣电池工作数月甚至数年。蓝牙 v4.0 引入了这项技术。支持该技术的设备被称为蓝牙智能就绪设备。该技术的主要特点如下
- 超低峰值、平均和空闲模式功耗
- 使用标准纽扣电池可运行数年
- 低成本
- 多供应商互操作性
- 范围更广
蓝牙低功耗采用客户端-服务器架构。服务器(也称为外围设备)提供温度或心率等服务,并对其进行宣传。客户端(称为中央设备)连接到服务器,并读取服务器发布的数值。举个例子,公寓里可能装有蓝牙智能就绪传感器,如恒温器、湿度或压力传感器。这些传感器是宣传公寓环境值的外围设备。与此同时,手机或电脑可能会连接到这些传感器,检索它们的数值,并将其作为更大的环境控制应用程序的一部分呈现给用户。
基本服务结构
蓝牙低功耗基于两个协议:ATT(属性协议)和 GATT(通用属性配置文件)。它们规定了每个蓝牙智能就绪设备使用的通信层。
ATT 协议
ATT 的基本构件是属性。每个属性由三个元素组成:
- 值--有效载荷或所需信息
- UUID - 属性类型(GATT 使用)
- 一个 16 位句柄--属性的唯一标识符
服务器存储属性,客户端使用 ATT 协议读写服务器上的值。
GATT 简介
GATT 通过应用预定义 UUID 的含义来定义一组属性的分组。下表显示了一个服务示例,其中公开了某一天的心率。实际值存储在两个特征中:
处理 | UUID | 值 | 说明 |
---|---|---|---|
0x0001 | 0x2800 | UUID 0x180D | 开始心率服务 |
0x0002 | 0x2803 | UUID 0x2A37, 值句柄:0x0003 | 心率测量(HRM)类型的特性 |
0x0003 | 0x2A37 | 65 bpm | 心率值 |
0x0004 | 0x2803 | UUID 0x2A08,值句柄:0x0005 | 日期时间类型特征 |
0x0005 | 0x2A08 | 18/08/2014 11:00 | 测量日期和时间 |
0x0006 | 0x2800 | UUID xxxxxx | 开始下一次服务 |
... | ... | ... | ... |
GATT 规定,上述使用的 UUID0x2800
标志着服务定义的开始。0x2800
之后的每个属性都是服务的一部分,直到下一个0x2800
或结束。众所周知的 UUID0x2803
也以类似的方式说明了要查找的特征,每个特征都有一个定义值性质的类型。上例中使用的 UUID 是0x2A08
(日期时间)和0x2A37
(心率测量)。上述每个 UUID 都由蓝牙特殊兴趣小组定义,可在GATT 规范中找到。建议在可用的情况下使用预定义的 UUID,也完全可以为特性和服务类型使用新的和尚未使用的 UUID。
一般来说,每项服务都可能包含一个或多个特征。特征包含数据,并可由描述符进一步描述,描述符提供附加信息或操作特征的方法。所有服务、特征和描述符都可以通过 128 位 UUID 进行识别。最后,服务内部还可以包含服务(见下图)。
使用Qt Bluetooth 低能耗应用程序接口
本节将介绍如何使用 Qt Bluetooth 提供的低能耗 API。在客户端,API 允许创建与外围设备的连接、发现其服务以及读写设备上存储的数据。在服务器端,它允许设置服务、宣传服务并在客户端写入特征时获得通知。下面的示例代码取自心率游戏和心率服务器示例。
建立连接
要读写蓝牙低功耗外围设备的特性,必须找到并连接该设备。这需要外围设备公布其存在和服务。我们借助QBluetoothDeviceDiscoveryAgent 类开始设备发现。我们连接到QBluetoothDeviceDiscoveryAgent::deviceDiscovered() 信号,并通过start() 开始搜索:
m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(15000); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &DeviceFinder::addDevice); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, &DeviceFinder::scanError); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &DeviceFinder::scanFinished); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &DeviceFinder::scanFinished); m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
由于我们只对低能耗设备感兴趣,因此我们会过滤接收槽内的设备类型。设备类型可通过QBluetoothDeviceInfo::coreConfigurations() 标志确定。随着发现更多细节,同一设备可能会多次发出deviceDiscovered() 信号。在此,我们将匹配这些设备发现,以便用户只看到单个设备:
void DeviceFinder::addDevice(const QBluetoothDeviceInfo &device) { // If device is LowEnergy-device, add it to the list if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) { auto devInfo = new DeviceInfo(device); auto it = std::find_if(m_devices.begin(), m_devices.end(), [devInfo](DeviceInfo *dev) { return devInfo->getAddress() == dev->getAddress(); }); if (it == m_devices.end()) { m_devices.append(devInfo); } else { auto oldDev = *it; *it = devInfo; delete oldDev; } setInfo(tr("Low Energy device found. Scanning more...")); setIcon(IconProgress); } //... }
一旦知道了外围设备的地址,我们就可以使用QLowEnergyController 类。该类是所有蓝牙低功耗开发的入口。该类的构造函数接受远程设备的QBluetoothAddress 。最后,我们使用connectToDevice() 设置常用插槽并直接连接到设备:
m_control = QLowEnergyController::createCentral(m_currentDevice->getDevice(), this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &DeviceHandler::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &DeviceHandler::serviceScanDone); connect(m_control, &QLowEnergyController::errorOccurred, this, [this](QLowEnergyController::Error error) { Q_UNUSED(error); setError("Cannot connect to remote device."); setIcon(IconError); }); connect(m_control, &QLowEnergyController::connected, this, [this]() { setInfo("Controller connected. Search services..."); setIcon(IconProgress); m_control->discoverServices(); }); connect(m_control, &QLowEnergyController::disconnected, this, [this]() { setError("LowEnergy controller disconnected"); setIcon(IconError); }); // Connect m_control->connectToDevice();
服务搜索
上面的代码片段显示了连接建立后应用程序如何启动服务搜索。
下面的serviceDiscovered()
插槽由QLowEnergyController::serviceDiscovered() 信号触发,并提供间歇性进度报告。由于我们讨论的是监控附近 HeartRate 设备的心脏监听器应用程序,因此我们忽略了任何非QBluetoothUuid::ServiceClassUuid::HeartRate 类型的服务。
void DeviceHandler::serviceDiscovered(const QBluetoothUuid &gatt) { if (gatt == QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate)) { setInfo("Heart Rate service discovered. Waiting for service scan to be done..."); setIcon(IconProgress); m_foundHeartRateService = true; } }
最终,QLowEnergyController::discoveryFinished() 信号发出,表明服务发现成功完成。如果找到了 HeartRate 服务,就会创建一个QLowEnergyService 实例来表示该服务。返回的服务对象会提供更新通知所需的信号,并使用QLowEnergyService::discoverDetails() 触发服务细节的发现:
// If heartRateService found, create new service if (m_foundHeartRateService) m_service = m_control->createServiceObject(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate), this); if (m_service) { connect(m_service, &QLowEnergyService::stateChanged, this, &DeviceHandler::serviceStateChanged); connect(m_service, &QLowEnergyService::characteristicChanged, this, &DeviceHandler::updateHeartRateValue); connect(m_service, &QLowEnergyService::descriptorWritten, this, &DeviceHandler::confirmedDescriptorWrite); m_service->discoverDetails(); } else { setError("Heart Rate Service not found."); setIcon(IconError); }
在详细搜索过程中,服务的state() 会从RemoteService 过渡到RemoteServiceDiscovering ,并最终以RemoteServiceDiscovered 结束:
void DeviceHandler::serviceStateChanged(QLowEnergyService::ServiceState s) { switch (s) { case QLowEnergyService::RemoteServiceDiscovering: setInfo(tr("Discovering services...")); setIcon(IconProgress); break; case QLowEnergyService::RemoteServiceDiscovered: { setInfo(tr("Service discovered.")); setIcon(IconBluetooth); const QLowEnergyCharacteristic hrChar = m_service->characteristic(QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement)); if (!hrChar.isValid()) { setError("HR Data not found."); setIcon(IconError); break; } m_notificationDesc = hrChar.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); if (m_notificationDesc.isValid()) m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100")); break; } default: //nothing for now break; } emit aliveChanged(); }
与外围设备交互
在上面的代码示例中,所需的特征类型为HeartRateMeasurement 。由于应用程序测量的是心率变化,因此必须启用该特性的变化通知。请注意,并非所有特性都提供更改通知。由于 HeartRate 特性已经标准化,因此可以假定可以收到通知。最终QLowEnergyCharacteristic::properties() 必须设置QLowEnergyCharacteristic::Notify 标志,并且必须存在QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration 类型的描述符,以确认是否有适当的通知。
最后,我们根据蓝牙低功耗标准处理 HeartRate 特性的值:
void DeviceHandler::updateHeartRateValue(const QLowEnergyCharacteristic &c, const QByteArray &value) { // ignore any other characteristic change -> shouldn't really happen though if (c.uuid() != QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement)) return; auto data = reinterpret_cast<const quint8 *>(value.constData()); quint8 flags = *data; //Heart Rate int hrvalue = 0; if (flags & 0x1) // HR 16 bit? otherwise 8 bit hrvalue = static_cast<int>(qFromLittleEndian<quint16>(data[1])); else hrvalue = static_cast<int>(data[1]); addMeasurement(hrvalue); }
一般来说,特征值是一系列字节。这些字节的精确解释取决于特征类型和值结构。蓝牙技术联盟(Bluetooth SIG)已对大量数据进行了标准化,而其他数据则可能遵循自定义协议。上面的代码片段演示了如何读取标准化的 HeartRate 值。
广告服务
如果我们要在外围设备上实施 GATT 服务器应用程序,我们就需要定义要向中央设备提供的服务并对其进行宣传:
QLowEnergyAdvertisingDataadvertisingData; advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); advertisingData.setIncludePowerLevel(true); advertisingData.setLocalName("HeartRateServer"); advertisingData.setServices(QList<QBluetoothUuid>()<<QBluetoothUuid::ServiceClassUuid::HeartRate);boolerrorOccurred= false;conststd::unique_ptr<QLowEnergyController>leController(QLowEnergyController::createPeripheral());autoerrorHandler= [&leController, &errorOccurred](QLowEnergyController::Error errorCode) { qWarning().noquote().nospace() << errorCode << " occurred: " << leController->errorString();if(errorCode!=) QLowEnergyController::RemoteHostClosedError) { qWarning("Heartrate-server quitting due to the error."); errorOccurred= true; QCoreApplication::quit(); } };QObject::connect(leController.get(), &QLowEnergyController::errorOccurred,errorHandler); std::unique_ptr<QLowEnergyService> 服务(leController->addService(serviceData)); leController->startAdvertising(QLowEnergyAdvertisingParameters(),advertisingData,advertisingData);if(errorOccurred)return-1;
现在,潜在客户可以连接到我们的设备,发现所提供的服务并进行注册,以便在特征值发生变化时获得通知。这部分应用程序接口已在上述章节中介绍过。
在外围设备上实现服务
第一步是定义服务、其特征和描述符。这可以通过QLowEnergyServiceData 、QLowEnergyCharacteristicData 和QLowEnergyDescriptorData 类来实现。这些类充当容器或构件,用于存储构成待定义蓝牙低功耗服务的基本信息。下面的代码片段定义了一个简单的 HeartRate 服务,用于发布测量到的每分钟心跳次数。手表就是使用这种服务的一个例子。
QLowEnergyCharacteristicData charData; charData.setUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement); charData.setValue(QByteArray(2, 0)); charData.setProperties(QLowEnergyCharacteristic::Notify); const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration, QByteArray(2, 0)); charData.addDescriptor(clientConfig); QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::HeartRate); serviceData.addCharacteristic(charData);
由此产生的serviceData
对象可按上文广告服务部分所述进行发布。尽管QLowEnergyServiceData 和QLowEnergyAdvertisingData 封装的信息有部分重叠,但这两类信息服务于两种截然不同的任务。广告数据是向附近的设备发布的,由于其大小限制为 29 字节,因此范围往往有限。因此,它们并不总是 100% 完整的。相比之下,包含在QLowEnergyServiceData 中的服务数据则提供了完整的服务数据集,只有在与活动服务发现进行连接时,连接客户端才会看到这些数据。
下一节将演示服务如何更新心率值。根据服务的性质,它可能必须符合https://www.bluetooth.org 上定义的官方服务定义。其他服务可能完全是定制的。心率服务已被采用,其规范可在https://www.bluetooth.com/specifications/adopted-specifications 下找到。
QTimer heartbeatTimer; quint8 currentHeartRate = 60; enum ValueChange { ValueUp, ValueDown } valueChange = ValueUp; const auto heartbeatProvider = [&service, ¤tHeartRate, &valueChange]() { QByteArray value; value.append(char(0)); // Flags that specify the format of the value. value.append(char(currentHeartRate)); // Actual value. QLowEnergyCharacteristic characteristic = service->characteristic(QBluetoothUuid::CharacteristicType::HeartRateMeasurement); Q_ASSERT(characteristic.isValid()); service->writeCharacteristic(characteristic, value); // Potentially causes notification. if (currentHeartRate == 60) valueChange = ValueUp; else if (currentHeartRate == 100) valueChange = ValueDown; if (valueChange == ValueUp) ++currentHeartRate; else --currentHeartRate; }; QObject::connect(&heartbeatTimer, &QTimer::timeout, heartbeatProvider); heartbeatTimer.start(1000);
一般来说,外围设备上的特征和描述符值更新使用的方法与蓝牙低功耗设备的连接方法相同。
注: 要在 Qt Bluetooth(中央和外围设备角色),必须提供包含使用说明的 Info.plist 文件。根据 CoreBluetooth 文档:如果应用程序的 Info.plist 文件不包含需要访问的数据类型的使用说明密钥,应用程序就会崩溃。要在 iOS 13 或之后链接的应用程序上访问 Core Bluetooth API,请包含 NSBluetoothAlwaysUsageDescription 密钥。在 iOS 12 及更早版本中,请包含 NSBluetoothPeripheralUsageDescription 以访问蓝牙外设数据。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.