警告

本节包含从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 都由 Bluetooth Special Interest Group 定义,并且可以在 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。由于应用程序测量心率变化,因此必须为该特征启用变化通知。请注意,并非所有特征都提供变化通知。由于HeartRate特征已经标准化,可以假设可以接收到通知。最终properties()必须设置Notify标志,并且必须存在类型为ClientCharacteristicConfiguration的描述符以确认适当通知的可用性。

最后,我们按照蓝牙低能耗标准处理HeartRate特性的值:

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)

通常,特征值是一系列字节。这些字节的精确解释取决于特征类型和值结构。许多特征值已被Bluetooth SIG标准化,而其他特征值可能遵循自定义协议。上面的代码片段展示了如何读取标准化的心率值。

广告服务

如果我们在外围设备上实现一个GATT服务器应用程序,我们需要定义我们想要提供给中央设备的服务并广播它们:

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对象可以按照上面Advertising Services部分所述进行发布。尽管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 += 1
else:
            currentHeartRate -= 1

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

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

注意

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