低功耗蓝牙概述

Qt 低功耗蓝牙 API 支持外围/服务器和中心/客户端角色。它在所有主要 Qt 平台上得到支持。唯一的例外是在 Windows 上不支持外围角色。

什么是低功耗蓝牙

低功耗蓝牙,也称为蓝牙智能,是一种无线计算机网络技术,于 2011 年正式推出。它与“经典”蓝牙使用相同的 2.4 GHz 频率。主要区别在于,正如其技术名称所说,低功耗。它为低功耗蓝牙设备在纽扣电池上运行数月甚至数年提供了可能。该技术由 Bluetooth v4.0 引入。支持这项技术的设备被称为蓝牙智能设备。该技术的关键特性包括

  • 峰值、平均和空闲模式极低功耗
  • 能够使用标准纽扣电池运行数年
  • 低成本
  • 多厂商互操作性
  • 增强范围

低功耗蓝牙使用客户端-服务器架构。服务器(也称为外围设备)提供温度或心率等服务,并对其进行宣传。客户端(称为中心设备)连接到服务器,并读取服务器宣传的值。一个例子可能是一个具有蓝牙智能准备好传感器的公寓,例如恒温器、湿度和压力传感器。这些传感器是宣传公寓环境值的外围设备。同时,一部手机或电脑可能连接到这些传感器,检索其值,并将它们作为更大的环境控制系统的一部分呈现给用户。

基本服务结构

低功耗蓝牙基于两个协议:ATT(属性协议)和GATT(通用属性配置文件)。它们指定了每个蓝牙智能准备好设备使用的通信层。

ATT 协议

ATT 的基本构建块是一个 属性。每个属性由三个元素组成

  • 一个值 -有效载荷或所需的信息
  • 一个 UUID -属性的类型(由 GATT 使用)
  • 一个 16 位句柄 -属性的唯一标识符

服务器存储属性,客户端使用 ATT 协议在服务器上读取和写入值。

GATT 配置文件

通过为预定义的 UUIDs 应用含义,GATT 定义了一组属性的组合。下表显示了暴露特定一天心率的服务示例。实际值存储在两个特征值中

句柄UUID描述
0x00010x2800UUID 0x180D开始心率服务
0x00020x2803UUID 0x2A37,值句柄:0x0003类型为 心率测量 (HRM) 的特征
0x00030x2A3765 bpm心率值
0x00040x2803UUID 0x2A08,值句柄:0x0005日期时间类型的特征
0x00050x2A0818/08/2014 11:00测量日期和时间
0x00060x2800UUID xxxxxx开始下一个服务
............

根据GATT规定,上面使用的UUID 0x2800 标记了服务定义的开始。从 0x2800 开始的所有属性都属于该服务,直到遇到下一个 0x2800 或结束时。类似地,已知的UUID 0x2803 表示可以找到特征,并且每个特征都有一个定义值的类型的类型。上面的例子使用了UUID 0x2A08(日期时间)和 0x2A37(心率测量)。上述每个UUID都由 蓝牙特别兴趣小组 定义,可以在 GATT规范 中找到。建议在可能的情况下使用预定义的UUID,但完全可以使用新的、尚未使用的UUID作为特征和服务类型。

一般来说,每个服务可能包含一个或多个特征。一个特征包含数据,并且可以通过描述符进一步描述,描述符可以提供额外的信息或操作特征的方法。所有服务、特征和描述符都通过它们的128位UUID来识别。最后,还可以在服务内部包含服务(见下面的图片)。

使用Qt蓝牙低功耗API

本节描述了如何使用Qt提供的蓝牙低功耗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()标志来确定。《a href="qbluetoothdevicediscovered.html#deviceDiscovered" translate="no">设备接下来可能会被重复发射多次对于相同设备,因为它会发现更多详细信息。我们在这里匹配这些设备发现,以便用户只看到单个设备

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() 信号触发,并提供 intermittent 的进度报告。由于我们谈论的是监视附近心率器件的心脏监听应用程序,我们忽略任何非 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() 信号以指示服务发现的成功完成。如果找到心率服务,则创建一个 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);
    }

在详细搜索过程中,服务的状态从 RemoteServiceRemoteServiceDiscovering 转变,最终以 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。由于应用程序测量心率变化,它必须为特征启用更改通知。请注意,并非所有特征都提供更改通知。由于心率特征已被标准化,因此可以假设可以收到通知。最终,必须在 QLowEnergyCharacteristic::properties() 中设置 QLowEnergyCharacteristic::Notify 标志,并存在一个类型为 QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration 的描述符以确认适当的可用性。

最后,我们按蓝牙低功耗标准处理心率特征值。

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 标准化,而其他可能遵循自定义协议。上面的代码片段演示了如何读取标准化的心率值。

广告服务

如果我们在一个外围设备上实现 GATT 服务器应用程序,我们需要定义我们想向中心设备提供的服务并将它们广告出去

QLowEnergyAdvertisingData advertisingData;
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
advertisingData.setLocalName("HeartRateServer");
advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid::ServiceClassUuid::HeartRate);
bool errorOccurred = false;
const std::unique_ptr<QLowEnergyController> leController(QLowEnergyController::createPeripheral());
auto errorHandler = [&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> service(leController->addService(serviceData));
leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData,
                               advertisingData);
if (errorOccurred)
    return -1;

现在潜在的客户可以连接到我们的设备,发现提供的服务并注册自己来接收特征值更改的通知。本 API 的一部分已在上述部分中介绍。

在外围设备上实现服务

第一步是定义服务,其特征和描述符。这是通过使用 QLowEnergyServiceDataQLowEnergyCharacteristicDataQLowEnergyDescriptorData 类来实现的。这些类作为容器或构建块,用于包含即将定义的蓝牙低功耗服务的基本信息。下面的代码片段定义了一个简单的发布测量每分钟脉搏次数的心率服务。此类服务可以用作手表等设备。

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 对象可以按照上述 广告服务 部分的描述进行发布。尽管 QLowEnergyServiceDataQLowEnergyAdvertisingData 包装的信息之间存在部分重叠,但这两个类执行的是两个非常不同的任务。广告数据发布到附近的设备,通常由于其大小限制为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, &currentHeartRate, &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);

通常,外围设备上的特性值和描述符值更新使用与连接低功耗蓝牙设备相同的方法。

注意:要在iOS上使用Qt蓝牙(在中心和外围角色中)必须提供一个包含使用描述的Info.plist文件。根据CoreBluetooth的文档:如果您的Info.plist不包括访问所需数据的类型的使用描述关键,则您的应用程序将崩溃。要访问iOS 13或更高版本上链接的应用程序的Core Bluetooth API,请包含NSBluetoothAlwaysUsageDescription密钥。在iOS 12和更早版本中,包含NSBluetoothPeripheralUsageDescription以访问蓝牙外围设备数据。

© 2024 The Qt Company Ltd。本文件中包含的文档贡献的版权归其各自的拥有者。此处提供的文档根据自由软件基金会的条款在GNU自由文档许可证版本1.3下发布。Qt和相应的商标是The Qt Company Ltd.在芬兰以及全球其他国家的商标。所有其他商标均属于其各自的所有者。