警告
本部分包含自动从 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识别。最后,可以在服务内包含服务(见图下)。
使用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()
服务搜索#
上面的代码片段展示了在建立连接后,应用程序如何初始化服务发现。
下面的 serviceDiscovered()
插槽作为 serviceDiscovered()
信号的触发并提供了间歇性的进度报告。因为我们正在谈论的心率监听应用程序,它监控附近的心率设备,所以我们忽略任何非心率类型的其他服务。
def serviceDiscovered(self, gatt): if gatt == QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate): setInfo("Heart Rate service discovered. Waiting for service scan to be done...") setIcon(IconProgress) m_foundHeartRateService = True
最终,discoveryFinished()
信号发出,以指示服务发现的成功完成。如果在心率服务被发现的情况下,创建了一个 QLowEnergyService
实例来表示该服务。返回的服务对象提供了所需的更新通知信号,并使用 discoverDetails()
触发服务详细信息的发现
# If heartRateService found, create new service if m_foundHeartRateService: m_service = m_control.createServiceObject(QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate), self) if m_service: m_service.stateChanged.connect(self.serviceStateChanged) m_service.characteristicChanged.connect(self.updateHeartRateValue) m_service.descriptorWritten.connect(self.confirmedDescriptorWrite) m_service.discoverDetails() else: setError("Heart Rate Service not found.") setIcon(IconError)
在详细搜索期间,服务状态从 RemoteService
过渡到 RemoteServiceDiscovering
,最终结束于 RemoteServiceDiscovered
def serviceStateChanged(self, s): if s == QLowEnergyService.RemoteServiceDiscovering: setInfo(tr("Discovering services...")) setIcon(IconProgress) break elif s == QLowEnergyService.RemoteServiceDiscovered: setInfo(tr("Service discovered.")) setIcon(IconBluetooth) hrChar = m_service.characteristic(QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)) if not 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 else: #nothing for now break aliveChanged.emit()
与外围设备交互#
在上面的代码示例中,所需的特征类型为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已经在上述章节中介绍过。
在外围设备上实现服务#
第一步是定义服务,其特征和描述符。这使用QLowEnergyServiceData
,QLowEnergyCharacteristicData
和QLowEnergyDescriptorData
类实现。这些类作为容器或构建块,用于包含将要定义的蓝牙低能耗服务的基本信息。下面的代码片段定义了一个简单的心率服务,该服务发布每分钟测量的跳动次数。这种服务可以用于手表等设备中的示例。
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
对象可以按照上文“广告服务”部分所述进行发布。尽管 QLowEnergyServiceData
和 QLowEnergyAdvertisingData
包裹的信息存在部分重叠,但这两个类执行的是非常不同的任务。广告数据发布到附近的设备,通常由于其大小限制为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以访问蓝牙外围数据。