警告

本部分包含自动从 C++ 转换到 Python 的代码片段,可能包含错误。

蓝牙低功耗概述#

Qt 蓝牙低功耗 API 允许蓝牙低功耗设备之间进行通信。

Qt 蓝牙低功耗 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规定了上述使用的UUID 0x2800 标记服务定义的开始。从0x2800之后的所有属性都是服务的一部分,直到遇到下一个0x2800或达到结束。以类似的方式,已知的UUID 0x2803 表示将发现一个特征,并且每个特征都有一种类来定义值的性质。上述示例使用的是UUID 0x2A08(日期时间)和0x2A37(心率测量)。上述每个UUID都由蓝牙特别兴趣组 定义,并可在GATT规范 中找到。虽然建议在使用预先定义的UUID时优先使用,但完全可以为特征和服务类型使用新且尚未使用的UUID。

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

../_images/peripheral-structure1.png

使用Qt蓝牙低功耗API#

本节描述了如何使用Qt提供的蓝牙低功耗API。在客户端,该API允许创建与外围设备的连接,发现其服务,以及读取和写入设备上存储的数据。在服务器端,它允许设置服务、广告服务,并在客户端写入特征时进行通知。以下示例代码摘自心率游戏心率服务器示例。

建立连接#

要能够读取和写入蓝牙低功耗外围设备的特征,需要找到并连接该设备。这要求外围设备宣布其存在并提供服务。我们使用QBluetoothDeviceDiscoveryAgent类开始设备发现。我们连接到其deviceDiscovered()信号,并使用start()开始搜索。

m_deviceDiscoveryAgent = QBluetoothDeviceDiscoveryAgent(self)
m_deviceDiscoveryAgent.setLowEnergyDiscoveryTimeout(15000)
m_deviceDiscoveryAgent.deviceDiscovered.connect(
        self.addDevice)
m_deviceDiscoveryAgent.errorOccurred.connect(
        self.scanError)
m_deviceDiscoveryAgent.finished.connect(
        self.scanFinished)
m_deviceDiscoveryAgent.canceled.connect(
        self.scanFinished)
m_deviceDiscoveryAgent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)

由于我们只对低功耗设备感兴趣,因此我们在接收槽内过滤设备类型。可以使用 coreConfigurations() 标志确认设备类型。对于同一设备,由于会发现更多信息,deviceDiscovered() 信号可能会多次发出。在这里,我们匹配这些设备发现,以便用户只能看到单个设备

def addDevice(self, device):

    # If device is LowEnergy-device, add it to the list
    if device.coreConfigurations()  QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
        devInfo = DeviceInfo(device)
        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:
            oldDev = it
            it = devInfo
            del oldDev

        setInfo(tr("Low Energy device found. Scanning more..."))
        setIcon(IconProgress)
#...

一旦知道外围设备的地址,我们使用 QLowEnergyController 类。此类是所有蓝牙低功耗开发的起点。该类的构造函数接受远程设备的 QBluetoothAddress 。最后,我们设置常规插槽,并直接使用 connectToDevice() 连接到设备

m_control = QLowEnergyController.createCentral(m_currentDevice.getDevice(), self)
m_control.serviceDiscovered.connect(
        self.serviceDiscovered)
m_control.discoveryFinished.connect(
        self.serviceScanDone)
m_control.errorOccurred.connect(this,
        [self](QLowEnergyController.Error error) {
            Q_UNUSED(error)
            setError("Cannot connect to remote device.")
            setIcon(IconError)
        })
m_control.connected.connect(this, [this]() {
    setInfo("Controller connected. Search services...")
    setIcon(IconProgress)
    m_control.discoverServices()
})
m_control.disconnected.connect(this, [this]() {
    setError("LowEnergy controller disconnected")
    setIcon(IconError)
})
# Connect
m_control.connectToDevice()

与外围设备交互#

在上面的代码示例中,所需的特征类型为HeartRateMeasurement。由于应用程序测量心率变化,它必须启用特征的变化通知。注意,并非所有特征都提供变化通知。由于心率特征已经标准化,因此可以假设可以接收通知。最后,properties()必须设置Notify标志,并且必须存在类型为ClientCharacteristicConfiguration的描述符,以确认适当通知的可用性。

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

def updateHeartRateValue(self, c, value):

    # ignore any other characteristic change -> shouldn't really happen though
    if c.uuid() != QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement):
        return
    data = quint8(value.constData())
    flags = data
    #Heart Rate
    hrvalue = 0
    if flags  0x1: # HR 16 bit? otherwise 8 bit
        hrvalue = int(qFromLittleEndian<quint16>(data[1]))
else:
        hrvalue = int(data[1])
    addMeasurement(hrvalue)

通常,特征值是一系列字节。这些字节的确切解释取决于特征类型和值结构。蓝牙SIG已对这些字节的标准进行标准化,而其他可能遵循自定义协议。上述代码片段演示了如何读取标准的心率值。

广播服务#

如果我们正在外围设备上实现GATT服务器应用程序,我们需要定义要提供给集中式设备的服务和advertise these

advertisingData = QLowEnergyAdvertisingData()
advertisingData.setDiscoverability(QLowEnergyAdvertisingData.DiscoverabilityGeneral)
advertisingData.setIncludePowerLevel(True)
advertisingData.setLocalName("HeartRateServer")
advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid.ServiceClassUuid.HeartRate)
errorOccurred = False
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类实现。这些类作为容器或构建块,用于包含将要定义的蓝牙低能耗服务的基本信息。下面的代码片段定义了一个简单的心率服务,该服务发布每分钟测量的跳动次数。这种服务可以用于手表等设备中的示例。

charData = QLowEnergyCharacteristicData()
charData.setUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)
charData.setValue(QByteArray(2, 0))
charData.setProperties(QLowEnergyCharacteristic.Notify)
QLowEnergyDescriptorData clientConfig(QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration,
                                            QByteArray(2, 0))
charData.addDescriptor(clientConfig)
serviceData = QLowEnergyServiceData()
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 下找到。

    heartbeatTimer = QTimer()
    currentHeartRate = 60
    ValueChange = { ValueUp, ValueDown } valueChange = ValueUp
    auto heartbeatProvider = [service, currentHeartRate, valueChange]() {
        value = QByteArray()
        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
        elif currentHeartRate == 100:
            valueChange = ValueDown
        if valueChange == ValueUp:
            currentHeartRate = currentHeartRate + 1
else:
            currentHeartRate = currentHeartRate - 1

    heartbeatTimer.timeout.connect(heartbeatProvider)
    heartbeatTimer.start(1000)

通常情况下,外部设备上的特性和描述值更新使用与连接蓝牙低能耗设备相同的方法。

注意

要在iOS上使用 Qt蓝牙(无论是中央角色还是外围角色),必须提供一个包含使用描述的Info.plist文件。根据CoreBluetooth的文档:如果您的Info.plist不包含需要访问的数据类型的用法描述键,则应用程序将崩溃。为了访问iOS 13及以后链接的应用程序上的Core Bluetooth API,请包括NSBluetoothAlwaysUsageDescription键。在iOS 12及更早版本中,包括NSBluetoothPeripheralUsageDescription以访问蓝牙外围数据。