警告
本节包含从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来识别。最后,可以在服务中包含服务(见下图)。
使用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()信号触发的,并提供一个间歇性的进度报告。由于我们正在讨论的是监听附近心率设备的心率监听器应用程序,我们忽略任何不是HeartRate类型的服务。
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)
在详细搜索期间,服务的state()从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。由于应用程序测量心率变化,因此必须为该特征启用变化通知。请注意,并非所有特征都提供变化通知。由于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已经在上述部分中涵盖。
在外围设备上实现服务¶
第一步是定义服务、其特征和描述符。这是通过使用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对象可以按照上面Advertising Services部分所述进行发布。尽管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 += 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以访问蓝牙外围数据。