| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the QtBluetooth module of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include <QtBluetooth/qbluetoothaddress.h> |
| 30 | #include <QtBluetooth/qbluetoothdevicediscoveryagent.h> |
| 31 | #include <QtBluetooth/qbluetoothdeviceinfo.h> |
| 32 | #include <QtBluetooth/qbluetoothlocaldevice.h> |
| 33 | #include <QtBluetooth/qlowenergyadvertisingdata.h> |
| 34 | #include <QtBluetooth/qlowenergyadvertisingparameters.h> |
| 35 | #include <QtBluetooth/qlowenergyconnectionparameters.h> |
| 36 | #include <QtBluetooth/qlowenergycontroller.h> |
| 37 | #include <QtBluetooth/qlowenergycharacteristicdata.h> |
| 38 | #include <QtBluetooth/qlowenergydescriptordata.h> |
| 39 | #include <QtBluetooth/qlowenergyservicedata.h> |
| 40 | #include <QtCore/qendian.h> |
| 41 | #include <QtCore/qscopedpointer.h> |
| 42 | //#include <QtCore/qloggingcategory.h> |
| 43 | #include <QtTest/qsignalspy.h> |
| 44 | #include <QtTest/QtTest> |
| 45 | |
| 46 | #ifdef Q_OS_LINUX |
| 47 | #include <QtBluetooth/private/lecmaccalculator_p.h> |
| 48 | #endif |
| 49 | |
| 50 | #include <algorithm> |
| 51 | #include <cstring> |
| 52 | |
| 53 | using namespace QBluetooth; |
| 54 | |
| 55 | class TestQLowEnergyControllerGattServer : public QObject |
| 56 | { |
| 57 | Q_OBJECT |
| 58 | |
| 59 | private slots: |
| 60 | void initTestCase(); |
| 61 | |
| 62 | // Static, local stuff goes here. |
| 63 | void advertisingParameters(); |
| 64 | void advertisingData(); |
| 65 | void cmacVerifier(); |
| 66 | void cmacVerifier_data(); |
| 67 | void connectionParameters(); |
| 68 | void controllerType(); |
| 69 | void serviceData(); |
| 70 | |
| 71 | // Interaction with actual GATT server goes here. Order is relevant. |
| 72 | void advertisedData(); |
| 73 | void serverCommunication(); |
| 74 | |
| 75 | private: |
| 76 | QBluetoothAddress m_serverAddress; |
| 77 | QBluetoothDeviceInfo m_serverInfo; |
| 78 | QScopedPointer<QLowEnergyController> m_leController; |
| 79 | |
| 80 | #if defined(CHECK_CMAC_SUPPORT) |
| 81 | bool checkCmacSupport(const quint128& csrkMsb); |
| 82 | #endif |
| 83 | }; |
| 84 | |
| 85 | |
| 86 | void TestQLowEnergyControllerGattServer::initTestCase() |
| 87 | { |
| 88 | //QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); |
| 89 | const QString serverAddress = qgetenv(varName: "QT_BT_GATTSERVER_TEST_ADDRESS" ); |
| 90 | if (serverAddress.isEmpty()) |
| 91 | return; |
| 92 | m_serverAddress = QBluetoothAddress(serverAddress); |
| 93 | QVERIFY(!m_serverAddress.isNull()); |
| 94 | } |
| 95 | |
| 96 | void TestQLowEnergyControllerGattServer::advertisingParameters() |
| 97 | { |
| 98 | QLowEnergyAdvertisingParameters params; |
| 99 | QCOMPARE(params, QLowEnergyAdvertisingParameters()); |
| 100 | QCOMPARE(params.filterPolicy(), QLowEnergyAdvertisingParameters::IgnoreWhiteList); |
| 101 | QCOMPARE(params.minimumInterval(), 1280); |
| 102 | QCOMPARE(params.maximumInterval(), 1280); |
| 103 | QCOMPARE(params.mode(), QLowEnergyAdvertisingParameters::AdvInd); |
| 104 | QVERIFY(params.whiteList().isEmpty()); |
| 105 | |
| 106 | params.setInterval(minimum: 100, maximum: 200); |
| 107 | QCOMPARE(params.minimumInterval(), 100); |
| 108 | QCOMPARE(params.maximumInterval(), 200); |
| 109 | params.setInterval(minimum: 200, maximum: 100); |
| 110 | QCOMPARE(params.minimumInterval(), 200); |
| 111 | QCOMPARE(params.maximumInterval(), 200); |
| 112 | |
| 113 | params.setMode(QLowEnergyAdvertisingParameters::AdvScanInd); |
| 114 | QCOMPARE(params.mode(), QLowEnergyAdvertisingParameters::AdvScanInd); |
| 115 | |
| 116 | const auto whiteList = QList<QLowEnergyAdvertisingParameters::AddressInfo>() |
| 117 | << QLowEnergyAdvertisingParameters::AddressInfo(QBluetoothAddress(), |
| 118 | QLowEnergyController::PublicAddress); |
| 119 | params.setWhiteList(whiteList, policy: QLowEnergyAdvertisingParameters::UseWhiteListForConnecting); |
| 120 | QCOMPARE(params.whiteList(), whiteList); |
| 121 | QCOMPARE(params.filterPolicy(), QLowEnergyAdvertisingParameters::UseWhiteListForConnecting); |
| 122 | QVERIFY(params != QLowEnergyAdvertisingParameters()); |
| 123 | |
| 124 | // verify default ctor |
| 125 | QLowEnergyAdvertisingParameters::AddressInfo info; |
| 126 | QVERIFY(info.address == QBluetoothAddress()); |
| 127 | QVERIFY(info.type == QLowEnergyController::PublicAddress); |
| 128 | } |
| 129 | |
| 130 | void TestQLowEnergyControllerGattServer::advertisingData() |
| 131 | { |
| 132 | QLowEnergyAdvertisingData data; |
| 133 | QCOMPARE(data, QLowEnergyAdvertisingData()); |
| 134 | QCOMPARE(data.discoverability(), QLowEnergyAdvertisingData::DiscoverabilityNone); |
| 135 | QCOMPARE(data.includePowerLevel(), false); |
| 136 | QCOMPARE(data.localName(), QString()); |
| 137 | QCOMPARE(data.manufacturerData(), QByteArray()); |
| 138 | QCOMPARE(data.manufacturerId(), QLowEnergyAdvertisingData::invalidManufacturerId()); |
| 139 | QVERIFY(data.services().isEmpty()); |
| 140 | |
| 141 | data.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityLimited); |
| 142 | QCOMPARE(data.discoverability(), QLowEnergyAdvertisingData::DiscoverabilityLimited); |
| 143 | |
| 144 | data.setIncludePowerLevel(true); |
| 145 | QCOMPARE(data.includePowerLevel(), true); |
| 146 | |
| 147 | data.setLocalName("A device name" ); |
| 148 | QCOMPARE(data.localName(), QString("A device name" )); |
| 149 | |
| 150 | data.setManufacturerData(id: 0xfffd, data: "some data" ); |
| 151 | QCOMPARE(data.manufacturerId(), quint16(0xfffd)); |
| 152 | QCOMPARE(data.manufacturerData(), QByteArray("some data" )); |
| 153 | |
| 154 | const auto services = QList<QBluetoothUuid>() << QBluetoothUuid::CurrentTimeService |
| 155 | << QBluetoothUuid::DeviceInformation; |
| 156 | data.setServices(services); |
| 157 | QCOMPARE(data.services(), services); |
| 158 | |
| 159 | QByteArray rawData(7, 'x'); |
| 160 | data.setRawData(rawData); |
| 161 | QCOMPARE(data.rawData(), rawData); |
| 162 | |
| 163 | QVERIFY(data != QLowEnergyAdvertisingData()); |
| 164 | } |
| 165 | |
| 166 | void TestQLowEnergyControllerGattServer::cmacVerifier() |
| 167 | { |
| 168 | #if defined(CONFIG_LINUX_CRYPTO_API) && defined(QT_BUILD_INTERNAL) && defined(CONFIG_BLUEZ_LE) |
| 169 | // Test data comes from spec v4.2, Vol 3, Part H, Appendix D.1 |
| 170 | const quint128 csrk = { |
| 171 | { 0x3c, 0x4f, 0xcf, 0x09, 0x88, 0x15, 0xf7, 0xab, |
| 172 | 0xa6, 0xd2, 0xae, 0x28, 0x16, 0x15, 0x7e, 0x2b } |
| 173 | }; |
| 174 | QFETCH(QByteArray, message); |
| 175 | QFETCH(quint64, expectedMac); |
| 176 | |
| 177 | #if defined(CHECK_CMAC_SUPPORT) |
| 178 | if (!checkCmacSupport(csrk)) { |
| 179 | QSKIP("Needed socket options not available. Running qemu?" ); |
| 180 | } |
| 181 | #endif |
| 182 | |
| 183 | const bool success = LeCmacCalculator().verify(message, csrk, expectedMac); |
| 184 | QVERIFY(success); |
| 185 | #else // CONFIG_LINUX_CRYPTO_API |
| 186 | QSKIP("CMAC verification test only applicable for developer builds on Linux " |
| 187 | "with BlueZ and crypto API" ); |
| 188 | #endif // Q_OS_LINUX |
| 189 | } |
| 190 | |
| 191 | #if defined(CHECK_CMAC_SUPPORT) |
| 192 | #include <sys/socket.h> |
| 193 | #include <linux/if_alg.h> |
| 194 | #include <unistd.h> |
| 195 | |
| 196 | bool TestQLowEnergyControllerGattServer::checkCmacSupport(const quint128& csrk) |
| 197 | { |
| 198 | bool retval = false; |
| 199 | #if defined(CONFIG_LINUX_CRYPTO_API) && defined(QT_BUILD_INTERNAL) && defined(CONFIG_BLUEZ_LE) |
| 200 | quint128 csrkMsb; |
| 201 | std::reverse_copy(std::begin(csrk.data), std::end(csrk.data), std::begin(csrkMsb.data)); |
| 202 | |
| 203 | int testSocket = socket(AF_ALG, SOCK_SEQPACKET, 0); |
| 204 | if (testSocket != -1) { |
| 205 | sockaddr_alg sa; |
| 206 | using namespace std; |
| 207 | memset(&sa, 0, sizeof sa); |
| 208 | sa.salg_family = AF_ALG; |
| 209 | strcpy(reinterpret_cast<char *>(sa.salg_type), "hash" ); |
| 210 | strcpy(reinterpret_cast<char *>(sa.salg_name), "cmac(aes)" ); |
| 211 | if (::bind(testSocket, reinterpret_cast<sockaddr *>(&sa), sizeof sa) != -1) { |
| 212 | if (setsockopt(testSocket, 279 /* SOL_ALG */, ALG_SET_KEY, csrkMsb.data, sizeof csrkMsb) != -1) { |
| 213 | retval = true; |
| 214 | } else { |
| 215 | QWARN("Needed socket options (SOL_ALG) not available" ); |
| 216 | } |
| 217 | } else { |
| 218 | QWARN("bind() failed for crypto socket:" ); |
| 219 | } |
| 220 | close(testSocket); |
| 221 | } else { |
| 222 | QWARN("Unable to create test socket" ); |
| 223 | } |
| 224 | #endif |
| 225 | return retval; |
| 226 | } |
| 227 | #endif |
| 228 | |
| 229 | void TestQLowEnergyControllerGattServer::cmacVerifier_data() |
| 230 | { |
| 231 | QTest::addColumn<QByteArray>(name: "message" ); |
| 232 | QTest::addColumn<quint64>(name: "expectedMac" ); |
| 233 | QTest::newRow(dataTag: "D1.1" ) << QByteArray() << Q_UINT64_C(0xbb1d6929e9593728); |
| 234 | QTest::newRow(dataTag: "D1.2" ) << QByteArray::fromHex(hexEncoded: "2a179373117e3de9969f402ee2bec16b" ) |
| 235 | << Q_UINT64_C(0x070a16b46b4d4144); |
| 236 | QByteArray messageD13 = QByteArray::fromHex(hexEncoded: "6bc1bee22e409f96e93d7e117393172aae2d8a57" |
| 237 | "1e03ac9c9eb76fac45af8e5130c81c46a35ce411" ); |
| 238 | std::reverse(first: messageD13.begin(), last: messageD13.end()); |
| 239 | QTest::newRow(dataTag: "D1.3" ) << messageD13 << Q_UINT64_C(0xdfa66747de9ae630); |
| 240 | QByteArray messageD14 = QByteArray::fromHex(hexEncoded: "6bc1bee22e409f96e93d7e117393172a" |
| 241 | "ae2d8a571e03ac9c9eb76fac45af8e51" |
| 242 | "30c81c46a35ce411e5fbc1191a0a52ef" |
| 243 | "f69f2445df4f9b17ad2b417be66c3710" ); |
| 244 | std::reverse(first: messageD14.begin(), last: messageD14.end()); |
| 245 | QTest::newRow(dataTag: "D1.4" ) << messageD14 << Q_UINT64_C(0x51f0bebf7e3b9d92); |
| 246 | } |
| 247 | |
| 248 | void TestQLowEnergyControllerGattServer::connectionParameters() |
| 249 | { |
| 250 | QLowEnergyConnectionParameters connParams; |
| 251 | QCOMPARE(connParams, QLowEnergyConnectionParameters()); |
| 252 | connParams.setIntervalRange(minimum: 8, maximum: 9); |
| 253 | QCOMPARE(connParams.minimumInterval(), double(8)); |
| 254 | QCOMPARE(connParams.maximumInterval(), double(9)); |
| 255 | connParams.setIntervalRange(minimum: 9, maximum: 8); |
| 256 | QCOMPARE(connParams.minimumInterval(), double(9)); |
| 257 | QCOMPARE(connParams.maximumInterval(), double(9)); |
| 258 | connParams.setLatency(50); |
| 259 | QCOMPARE(connParams.latency(), 50); |
| 260 | connParams.setSupervisionTimeout(1000); |
| 261 | QCOMPARE(connParams.supervisionTimeout(), 1000); |
| 262 | const QLowEnergyConnectionParameters cp2 = connParams; |
| 263 | QCOMPARE(cp2, connParams); |
| 264 | QLowEnergyConnectionParameters cp3; |
| 265 | QVERIFY(cp3 != connParams); |
| 266 | cp3 = connParams; |
| 267 | QCOMPARE(cp3, connParams); |
| 268 | } |
| 269 | |
| 270 | void TestQLowEnergyControllerGattServer::advertisedData() |
| 271 | { |
| 272 | if (m_serverAddress.isNull()) |
| 273 | QSKIP("No server address provided" ); |
| 274 | QBluetoothDeviceDiscoveryAgent discoveryAgent; |
| 275 | discoveryAgent.start(); |
| 276 | QSignalSpy spy(&discoveryAgent, SIGNAL(finished())); |
| 277 | QVERIFY(spy.wait(30000)); |
| 278 | const QList<QBluetoothDeviceInfo> devices = discoveryAgent.discoveredDevices(); |
| 279 | const auto it = std::find_if(first: devices.constBegin(), last: devices.constEnd(), |
| 280 | pred: [this](const QBluetoothDeviceInfo &device) { return device.address() == m_serverAddress; }); |
| 281 | QVERIFY(it != devices.constEnd()); |
| 282 | m_serverInfo = *it; |
| 283 | |
| 284 | // BlueZ seems to interfere with the advertising in some way, so that in addition to the name |
| 285 | // we set, the host name of the machine is also sent. Therefore we cannot guarantee that "our" |
| 286 | // name is seen on the scanning machine. |
| 287 | // QCOMPARE(m_serverInfo.name(), QString("Qt GATT server")); |
| 288 | |
| 289 | QVERIFY(m_serverInfo.serviceUuids().count() >= 3); |
| 290 | QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid::GenericAccess)); |
| 291 | QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid::RunningSpeedAndCadence)); |
| 292 | QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid(quint16(0x2000)))); |
| 293 | } |
| 294 | |
| 295 | void TestQLowEnergyControllerGattServer::serverCommunication() |
| 296 | { |
| 297 | if (m_serverAddress.isNull()) |
| 298 | QSKIP("No server address provided" ); |
| 299 | m_leController.reset(other: QLowEnergyController::createCentral(remoteDevice: m_serverInfo)); |
| 300 | QVERIFY(!m_leController.isNull()); |
| 301 | m_leController->connectToDevice(); |
| 302 | QScopedPointer<QSignalSpy> spy(new QSignalSpy(m_leController.data(), |
| 303 | &QLowEnergyController::connected)); |
| 304 | QVERIFY(spy->wait(30000)); |
| 305 | m_leController->discoverServices(); |
| 306 | spy.reset(other: new QSignalSpy(m_leController.data(), &QLowEnergyController::discoveryFinished)); |
| 307 | QVERIFY(spy->wait(30000)); |
| 308 | const QList<QBluetoothUuid> serviceUuids = m_leController->services(); |
| 309 | QCOMPARE(serviceUuids.count(), 4); |
| 310 | QVERIFY(serviceUuids.contains(QBluetoothUuid::GenericAccess)); |
| 311 | QVERIFY(serviceUuids.contains(QBluetoothUuid::RunningSpeedAndCadence)); |
| 312 | QVERIFY(serviceUuids.contains(QBluetoothUuid(quint16(0x2000)))); |
| 313 | QVERIFY(serviceUuids.contains(QBluetoothUuid(QString("c47774c7-f237-4523-8968-e4ae75431daf" )))); |
| 314 | |
| 315 | const QScopedPointer<QLowEnergyService> genericAccessService( |
| 316 | m_leController->createServiceObject(service: QBluetoothUuid::GenericAccess)); |
| 317 | QVERIFY(!genericAccessService.isNull()); |
| 318 | genericAccessService->discoverDetails(); |
| 319 | while (genericAccessService->state() != QLowEnergyService::ServiceDiscovered) { |
| 320 | spy.reset(other: new QSignalSpy(genericAccessService.data(), &QLowEnergyService::stateChanged)); |
| 321 | QVERIFY(spy->wait(3000)); |
| 322 | } |
| 323 | QCOMPARE(genericAccessService->includedServices().count(), 1); |
| 324 | QCOMPARE(genericAccessService->includedServices().first(), |
| 325 | QBluetoothUuid(QBluetoothUuid::RunningSpeedAndCadence)); |
| 326 | QCOMPARE(genericAccessService->characteristics().count(), 2); |
| 327 | const QLowEnergyCharacteristic deviceNameChar |
| 328 | = genericAccessService->characteristic(uuid: QBluetoothUuid::DeviceName); |
| 329 | QVERIFY(deviceNameChar.isValid()); |
| 330 | QCOMPARE(deviceNameChar.descriptors().count(), 0); |
| 331 | QCOMPARE(deviceNameChar.properties(), |
| 332 | QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write); |
| 333 | QCOMPARE(deviceNameChar.value().constData(), "Qt GATT server" ); |
| 334 | const QLowEnergyCharacteristic appearanceChar |
| 335 | = genericAccessService->characteristic(uuid: QBluetoothUuid::Appearance); |
| 336 | QVERIFY(appearanceChar.isValid()); |
| 337 | QCOMPARE(appearanceChar.descriptors().count(), 0); |
| 338 | QCOMPARE(appearanceChar.properties(), QLowEnergyCharacteristic::Read); |
| 339 | auto value = qFromLittleEndian<quint16>(src: reinterpret_cast<const uchar *>( |
| 340 | appearanceChar.value().constData())); |
| 341 | QCOMPARE(value, quint16(128)); |
| 342 | |
| 343 | const QScopedPointer<QLowEnergyService> runningSpeedService( |
| 344 | m_leController->createServiceObject(service: QBluetoothUuid::RunningSpeedAndCadence)); |
| 345 | QVERIFY(!runningSpeedService.isNull()); |
| 346 | runningSpeedService->discoverDetails(); |
| 347 | while (runningSpeedService->state() != QLowEnergyService::ServiceDiscovered) { |
| 348 | spy.reset(other: new QSignalSpy(runningSpeedService.data(), &QLowEnergyService::stateChanged)); |
| 349 | QVERIFY(spy->wait(3000)); |
| 350 | } |
| 351 | QCOMPARE(runningSpeedService->includedServices().count(), 0); |
| 352 | QCOMPARE(runningSpeedService->characteristics().count(), 2); |
| 353 | QLowEnergyCharacteristic measurementChar |
| 354 | = runningSpeedService->characteristic(uuid: QBluetoothUuid::RSCMeasurement); |
| 355 | QVERIFY(measurementChar.isValid()); |
| 356 | QCOMPARE(measurementChar.descriptors().count(), 1); |
| 357 | const QLowEnergyDescriptor clientConfigDesc |
| 358 | = measurementChar.descriptor(uuid: QBluetoothUuid::ClientCharacteristicConfiguration); |
| 359 | QVERIFY(clientConfigDesc.isValid()); |
| 360 | QCOMPARE(clientConfigDesc.value(), QByteArray(2, 0)); |
| 361 | QCOMPARE(measurementChar.properties(), QLowEnergyCharacteristic::Notify); |
| 362 | QCOMPARE(measurementChar.value(), QByteArray()); // Empty because Read property not set |
| 363 | QLowEnergyCharacteristic featureChar |
| 364 | = runningSpeedService->characteristic(uuid: QBluetoothUuid::RSCFeature); |
| 365 | QVERIFY(featureChar.isValid()); |
| 366 | QCOMPARE(featureChar.descriptors().count(), 0); |
| 367 | QCOMPARE(featureChar.properties(), QLowEnergyCharacteristic::Read); |
| 368 | value = qFromLittleEndian<quint16>(src: reinterpret_cast<const uchar *>( |
| 369 | featureChar.value().constData())); |
| 370 | QCOMPARE(value, quint16(1 << 2)); |
| 371 | |
| 372 | // 128 bit custom uuid service |
| 373 | QBluetoothUuid serviceUuid128(QString("c47774c7-f237-4523-8968-e4ae75431daf" )); |
| 374 | QBluetoothUuid charUuid128(QString("c0ad61b1-79e7-42f9-ace0-0a9aa0d0a4f8" )); |
| 375 | QScopedPointer<QLowEnergyService> customService128( |
| 376 | m_leController->createServiceObject(service: serviceUuid128)); |
| 377 | QVERIFY(!customService128.isNull()); |
| 378 | customService128->discoverDetails(); |
| 379 | while (customService128->state() != QLowEnergyService::ServiceDiscovered) { |
| 380 | spy.reset(other: new QSignalSpy(customService128.data(), &QLowEnergyService::stateChanged)); |
| 381 | QVERIFY(spy->wait(5000)); |
| 382 | } |
| 383 | QCOMPARE(customService128->serviceUuid(), serviceUuid128); |
| 384 | QCOMPARE(customService128->includedServices().count(), 0); |
| 385 | QCOMPARE(customService128->characteristics().count(), 1); |
| 386 | QLowEnergyCharacteristic customChar128 |
| 387 | = customService128->characteristic(uuid: charUuid128); |
| 388 | QVERIFY(customChar128.isValid()); |
| 389 | QCOMPARE(customChar128.descriptors().count(), 0); |
| 390 | QCOMPARE(customChar128.value(), QByteArray(15, 'a')); |
| 391 | |
| 392 | QScopedPointer<QLowEnergyService> customService( |
| 393 | m_leController->createServiceObject(service: QBluetoothUuid(quint16(0x2000)))); |
| 394 | QVERIFY(!customService.isNull()); |
| 395 | customService->discoverDetails(); |
| 396 | while (customService->state() != QLowEnergyService::ServiceDiscovered) { |
| 397 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::stateChanged)); |
| 398 | QVERIFY(spy->wait(5000)); |
| 399 | } |
| 400 | QCOMPARE(customService->includedServices().count(), 0); |
| 401 | QCOMPARE(customService->characteristics().count(), 5); |
| 402 | QLowEnergyCharacteristic customChar |
| 403 | = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5000))); |
| 404 | QVERIFY(customChar.isValid()); |
| 405 | QCOMPARE(customChar.descriptors().count(), 0); |
| 406 | QCOMPARE(customChar.value(), QByteArray(1024, 'x')); |
| 407 | |
| 408 | QLowEnergyCharacteristic customChar2 |
| 409 | = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5001))); |
| 410 | QVERIFY(customChar2.isValid()); |
| 411 | QCOMPARE(customChar2.descriptors().count(), 0); |
| 412 | QCOMPARE(customChar2.value(), QByteArray()); // Was not readable due to authorization requirement. |
| 413 | |
| 414 | QLowEnergyCharacteristic customChar3 |
| 415 | = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5002))); |
| 416 | QVERIFY(customChar3.isValid()); |
| 417 | QCOMPARE(customChar3.properties(), |
| 418 | QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Indicate); |
| 419 | QCOMPARE(customChar3.descriptors().count(), 1); |
| 420 | QLowEnergyDescriptor cc3ClientConfig |
| 421 | = customChar3.descriptor(uuid: QBluetoothUuid::ClientCharacteristicConfiguration); |
| 422 | QVERIFY(cc3ClientConfig.isValid()); |
| 423 | |
| 424 | QLowEnergyCharacteristic customChar4 |
| 425 | = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5003))); |
| 426 | QVERIFY(customChar4.isValid()); |
| 427 | QCOMPARE(customChar4.properties(), |
| 428 | QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify); |
| 429 | QCOMPARE(customChar4.descriptors().count(), 1); |
| 430 | QLowEnergyDescriptor cc4ClientConfig |
| 431 | = customChar4.descriptor(uuid: QBluetoothUuid::ClientCharacteristicConfiguration); |
| 432 | QVERIFY(cc4ClientConfig.isValid()); |
| 433 | |
| 434 | QLowEnergyCharacteristic customChar5 |
| 435 | = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5004))); |
| 436 | QVERIFY(customChar5.isValid()); |
| 437 | QCOMPARE(customChar5.properties(), |
| 438 | QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::WriteSigned); |
| 439 | QCOMPARE(customChar5.descriptors().count(), 0); |
| 440 | QCOMPARE(customChar5.value(), QByteArray("initial" )); |
| 441 | |
| 442 | customService->writeCharacteristic(characteristic: customChar, newValue: "whatever" ); |
| 443 | spy.reset(other: new QSignalSpy(customService.data(), static_cast<void (QLowEnergyService::*) |
| 444 | (QLowEnergyService::ServiceError)>(&QLowEnergyService::error))); |
| 445 | QVERIFY(spy->wait(3000)); |
| 446 | QCOMPARE(customService->error(), QLowEnergyService::CharacteristicWriteError); |
| 447 | |
| 448 | spy.reset(other: new QSignalSpy(customService.data(), static_cast<void (QLowEnergyService::*) |
| 449 | (QLowEnergyService::ServiceError)>(&QLowEnergyService::error))); |
| 450 | customService->writeCharacteristic(characteristic: customChar5, newValue: "1" , mode: QLowEnergyService::WriteSigned); |
| 451 | |
| 452 | // error might happen immediately or once event loop comes back |
| 453 | bool wasError = ((spy->count() > 0) || spy->wait(timeout: 3000)); // |
| 454 | |
| 455 | if (!wasError) { |
| 456 | // Signed write is done twice to test the sign counter stuff. |
| 457 | // Note: We assume here that the link is not encrypted, as this information is not exported. |
| 458 | customService->readCharacteristic(characteristic: customChar5); |
| 459 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::characteristicRead)); |
| 460 | QVERIFY(spy->wait(3000)); |
| 461 | QCOMPARE(customChar5.value(), QByteArray("1" )); |
| 462 | customService->writeCharacteristic(characteristic: customChar5, newValue: "2" , mode: QLowEnergyService::WriteSigned); |
| 463 | customService->readCharacteristic(characteristic: customChar5); |
| 464 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::characteristicRead)); |
| 465 | QVERIFY(spy->wait(3000)); |
| 466 | QCOMPARE(customChar5.value(), QByteArray("2" )); |
| 467 | } else { |
| 468 | QCOMPARE(customService->error(), QLowEnergyService::CharacteristicWriteError); |
| 469 | } |
| 470 | |
| 471 | QByteArray indicateValue(2, 0); |
| 472 | qToLittleEndian<quint16>(src: 2, dest: reinterpret_cast<uchar *>(indicateValue.data())); |
| 473 | customService->writeDescriptor(descriptor: cc3ClientConfig, newValue: indicateValue); |
| 474 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::descriptorWritten)); |
| 475 | QVERIFY(spy->wait(3000)); |
| 476 | |
| 477 | QByteArray notifyValue(2, 0); |
| 478 | qToLittleEndian<quint16>(src: 1, dest: reinterpret_cast<uchar *>(notifyValue.data())); |
| 479 | customService->writeDescriptor(descriptor: cc4ClientConfig, newValue: notifyValue); |
| 480 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::descriptorWritten)); |
| 481 | QVERIFY(spy->wait(3000)); |
| 482 | |
| 483 | // Server now changes the characteristic values. |
| 484 | |
| 485 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::characteristicChanged)); |
| 486 | QVERIFY(spy->wait(3000)); |
| 487 | if (spy->count() == 1) |
| 488 | QVERIFY(spy->wait(3000)); |
| 489 | QCOMPARE(customChar3.value().constData(), "indicated" ); |
| 490 | QCOMPARE(customChar4.value().constData(), "notified" ); |
| 491 | |
| 492 | // signal requires root privileges on Linux |
| 493 | spy.reset(other: new QSignalSpy(m_leController.data(), &QLowEnergyController::connectionUpdated)); |
| 494 | QVERIFY(spy->wait(5000)); |
| 495 | |
| 496 | m_leController->disconnectFromDevice(); |
| 497 | |
| 498 | if (m_leController->state() != QLowEnergyController::UnconnectedState) { |
| 499 | spy.reset(other: new QSignalSpy(m_leController.data(), &QLowEnergyController::stateChanged)); |
| 500 | QVERIFY(spy->wait(3000)); |
| 501 | } |
| 502 | QCOMPARE(m_leController->state(), QLowEnergyController::UnconnectedState); |
| 503 | |
| 504 | // Server now changes the characteristic values again while we're offline. |
| 505 | // Note: We cannot test indications and notifications for this case, as the client does |
| 506 | // not cache the old information and thus does not yet know the characteristics |
| 507 | // at the time the notification/indication is received. |
| 508 | |
| 509 | QTest::qWait(ms: 3000); |
| 510 | m_leController->connectToDevice(); |
| 511 | spy.reset(other: new QSignalSpy(m_leController.data(), &QLowEnergyController::connected)); |
| 512 | QVERIFY(spy->wait(30000)); |
| 513 | m_leController->discoverServices(); |
| 514 | spy.reset(other: new QSignalSpy(m_leController.data(), &QLowEnergyController::discoveryFinished)); |
| 515 | QVERIFY(spy->wait(30000)); |
| 516 | customService.reset(other: m_leController->createServiceObject(service: QBluetoothUuid(quint16(0x2000)))); |
| 517 | QVERIFY(!customService.isNull()); |
| 518 | customService->discoverDetails(); |
| 519 | while (customService->state() != QLowEnergyService::ServiceDiscovered) { |
| 520 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::stateChanged)); |
| 521 | QVERIFY(spy->wait(5000)); |
| 522 | } |
| 523 | customChar3 = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5002))); |
| 524 | QVERIFY(customChar3.isValid()); |
| 525 | QCOMPARE(customChar3.value().constData(), "indicated2" ); |
| 526 | customChar4 = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5003))); |
| 527 | QVERIFY(customChar4.isValid()); |
| 528 | QCOMPARE(customChar4.value().constData(), "notified2" ); |
| 529 | cc3ClientConfig = customChar3.descriptor(uuid: QBluetoothUuid::ClientCharacteristicConfiguration); |
| 530 | QVERIFY(cc3ClientConfig.isValid()); |
| 531 | cc4ClientConfig = customChar4.descriptor(uuid: QBluetoothUuid::ClientCharacteristicConfiguration); |
| 532 | QVERIFY(cc4ClientConfig.isValid()); |
| 533 | |
| 534 | const bool isBonded = QBluetoothLocalDevice().pairingStatus(address: m_serverAddress) |
| 535 | != QBluetoothLocalDevice::Unpaired; |
| 536 | if (isBonded) { |
| 537 | QCOMPARE(cc3ClientConfig.value(), indicateValue); |
| 538 | QCOMPARE(cc4ClientConfig.value(), notifyValue); |
| 539 | |
| 540 | // Do a third signed write to test sign counter persistence. |
| 541 | customChar5 = customService->characteristic(uuid: QBluetoothUuid(quint16(0x5004))); |
| 542 | QVERIFY(customChar5.isValid()); |
| 543 | QCOMPARE(customChar5.value(), QByteArray("2" )); |
| 544 | customService->writeCharacteristic(characteristic: customChar5, newValue: "3" , mode: QLowEnergyService::WriteSigned); |
| 545 | customService->readCharacteristic(characteristic: customChar5); |
| 546 | spy.reset(other: new QSignalSpy(customService.data(), &QLowEnergyService::characteristicRead)); |
| 547 | QVERIFY(spy->wait(3000)); |
| 548 | QCOMPARE(customChar5.value(), QByteArray("3" )); |
| 549 | |
| 550 | } else { |
| 551 | QCOMPARE(cc3ClientConfig.value(), QByteArray(2, 0)); |
| 552 | QCOMPARE(cc4ClientConfig.value(), QByteArray(2, 0)); |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | void TestQLowEnergyControllerGattServer::controllerType() |
| 557 | { |
| 558 | const QScopedPointer<QLowEnergyController> controller(QLowEnergyController::createPeripheral()); |
| 559 | QVERIFY(!controller.isNull()); |
| 560 | QCOMPARE(controller->role(), QLowEnergyController::PeripheralRole); |
| 561 | } |
| 562 | |
| 563 | void TestQLowEnergyControllerGattServer::serviceData() |
| 564 | { |
| 565 | QLowEnergyDescriptorData descData; |
| 566 | QVERIFY(!descData.isValid()); |
| 567 | |
| 568 | descData.setUuid(QBluetoothUuid::ValidRange); |
| 569 | QCOMPARE(descData.uuid(), QBluetoothUuid(QBluetoothUuid::ValidRange)); |
| 570 | QVERIFY(descData.isValid()); |
| 571 | QVERIFY(descData != QLowEnergyDescriptorData()); |
| 572 | |
| 573 | descData.setValue("xyz" ); |
| 574 | QCOMPARE(descData.value().constData(), "xyz" ); |
| 575 | |
| 576 | descData.setReadPermissions(readable: true, constraints: AttAuthenticationRequired); |
| 577 | QCOMPARE(descData.isReadable(), true); |
| 578 | QCOMPARE(descData.readConstraints(), AttAuthenticationRequired); |
| 579 | |
| 580 | descData.setWritePermissions(writable: false); |
| 581 | QCOMPARE(descData.isWritable(), false); |
| 582 | |
| 583 | QLowEnergyDescriptorData descData2(QBluetoothUuid::ReportReference, "abc" ); |
| 584 | QVERIFY(descData2 != QLowEnergyDescriptorData()); |
| 585 | QVERIFY(descData2.isValid()); |
| 586 | QCOMPARE(descData2.uuid(), QBluetoothUuid(QBluetoothUuid::ReportReference)); |
| 587 | QCOMPARE(descData2.value().constData(), "abc" ); |
| 588 | |
| 589 | QLowEnergyCharacteristicData charData; |
| 590 | QVERIFY(!charData.isValid()); |
| 591 | |
| 592 | charData.setUuid(QBluetoothUuid::BatteryLevel); |
| 593 | QVERIFY(charData != QLowEnergyCharacteristicData()); |
| 594 | QCOMPARE(charData.uuid(), QBluetoothUuid(QBluetoothUuid::BatteryLevel)); |
| 595 | QVERIFY(charData.isValid()); |
| 596 | |
| 597 | charData.setValue("value" ); |
| 598 | QCOMPARE(charData.value().constData(), "value" ); |
| 599 | |
| 600 | charData.setValueLength(minimum: 4, maximum: 7); |
| 601 | QCOMPARE(charData.minimumValueLength(), 4); |
| 602 | QCOMPARE(charData.maximumValueLength(), 7); |
| 603 | charData.setValueLength(minimum: 5, maximum: 2); |
| 604 | QCOMPARE(charData.minimumValueLength(), 5); |
| 605 | QCOMPARE(charData.maximumValueLength(), 5); |
| 606 | |
| 607 | const QLowEnergyCharacteristic::PropertyTypes props |
| 608 | = QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::WriteSigned; |
| 609 | charData.setProperties(props); |
| 610 | QCOMPARE(charData.properties(), props); |
| 611 | |
| 612 | charData.setReadConstraints(AttEncryptionRequired); |
| 613 | QCOMPARE(charData.readConstraints(), AttEncryptionRequired); |
| 614 | charData.setWriteConstraints(AttAuthenticationRequired | AttAuthorizationRequired); |
| 615 | QCOMPARE(charData.writeConstraints(), AttAuthenticationRequired | AttAuthorizationRequired); |
| 616 | |
| 617 | charData.addDescriptor(descriptor: descData); |
| 618 | QCOMPARE(charData.descriptors().count(), 1); |
| 619 | charData.setDescriptors(QList<QLowEnergyDescriptorData>()); |
| 620 | QCOMPARE(charData.descriptors().count(), 0); |
| 621 | charData.setDescriptors(QList<QLowEnergyDescriptorData>() << descData << descData2); |
| 622 | QLowEnergyDescriptorData descData3(QBluetoothUuid::ExternalReportReference, "someval" ); |
| 623 | charData.addDescriptor(descriptor: descData3); |
| 624 | charData.addDescriptor(descriptor: QLowEnergyDescriptorData()); // Invalid. |
| 625 | QCOMPARE(charData.descriptors(), |
| 626 | QList<QLowEnergyDescriptorData>() << descData << descData2 << descData3); |
| 627 | |
| 628 | QLowEnergyServiceData secondaryData; |
| 629 | QVERIFY(!secondaryData.isValid()); |
| 630 | |
| 631 | secondaryData.setUuid(QBluetoothUuid::SerialPort); |
| 632 | QCOMPARE(secondaryData.uuid(), QBluetoothUuid(QBluetoothUuid::SerialPort)); |
| 633 | QVERIFY(secondaryData.isValid()); |
| 634 | QVERIFY(secondaryData != QLowEnergyServiceData()); |
| 635 | |
| 636 | secondaryData.setType(QLowEnergyServiceData::ServiceTypeSecondary); |
| 637 | QCOMPARE(secondaryData.type(), QLowEnergyServiceData::ServiceTypeSecondary); |
| 638 | |
| 639 | secondaryData.addCharacteristic(characteristic: charData); |
| 640 | QCOMPARE(secondaryData.characteristics().count(), 1); |
| 641 | secondaryData.setCharacteristics(QList<QLowEnergyCharacteristicData>()); |
| 642 | QCOMPARE(secondaryData.characteristics().count(), 0); |
| 643 | secondaryData.setCharacteristics(QList<QLowEnergyCharacteristicData>() |
| 644 | << charData << QLowEnergyCharacteristicData()); |
| 645 | QCOMPARE(secondaryData.characteristics(), QList<QLowEnergyCharacteristicData>() << charData); |
| 646 | |
| 647 | QLowEnergyServiceData backupData; |
| 648 | backupData.setUuid(QBluetoothUuid::SerialPort); |
| 649 | QCOMPARE(backupData.uuid(), QBluetoothUuid(QBluetoothUuid::SerialPort)); |
| 650 | QVERIFY(backupData.isValid()); |
| 651 | QVERIFY(backupData != QLowEnergyServiceData()); |
| 652 | |
| 653 | backupData.setType(QLowEnergyServiceData::ServiceTypeSecondary); |
| 654 | QCOMPARE(backupData.type(), QLowEnergyServiceData::ServiceTypeSecondary); |
| 655 | |
| 656 | backupData.setCharacteristics(QList<QLowEnergyCharacteristicData>() |
| 657 | << charData << QLowEnergyCharacteristicData()); |
| 658 | QCOMPARE(backupData.characteristics(), QList<QLowEnergyCharacteristicData>() << charData); |
| 659 | QVERIFY(backupData == secondaryData); |
| 660 | |
| 661 | #ifdef Q_OS_DARWIN |
| 662 | QSKIP("GATT server functionality not implemented for Apple platforms" ); |
| 663 | #endif |
| 664 | const QScopedPointer<QLowEnergyController> controller(QLowEnergyController::createPeripheral()); |
| 665 | QVERIFY(!controller->addService(QLowEnergyServiceData())); |
| 666 | const QScopedPointer<QLowEnergyService> secondaryService(controller->addService(service: secondaryData)); |
| 667 | QVERIFY(!secondaryService.isNull()); |
| 668 | QCOMPARE(secondaryService->serviceUuid(), secondaryData.uuid()); |
| 669 | const QList<QLowEnergyCharacteristic> characteristics = secondaryService->characteristics(); |
| 670 | QCOMPARE(characteristics.count(), 1); |
| 671 | QCOMPARE(characteristics.first().uuid(), charData.uuid()); |
| 672 | const QList<QLowEnergyDescriptor> descriptors = characteristics.first().descriptors(); |
| 673 | QCOMPARE(descriptors.count(), 3); |
| 674 | const auto inUuids = QSet<QBluetoothUuid>() << descData.uuid() << descData2.uuid() |
| 675 | << descData3.uuid(); |
| 676 | QSet<QBluetoothUuid> outUuids; |
| 677 | for (const QLowEnergyDescriptor &desc : descriptors) |
| 678 | outUuids << desc.uuid(); |
| 679 | QCOMPARE(inUuids, outUuids); |
| 680 | |
| 681 | QLowEnergyServiceData primaryData; |
| 682 | primaryData.setUuid(QBluetoothUuid::Headset); |
| 683 | primaryData.addIncludedService(service: secondaryService.data()); |
| 684 | const QScopedPointer<QLowEnergyService> primaryService(controller->addService(service: primaryData)); |
| 685 | QVERIFY(!primaryService.isNull()); |
| 686 | QCOMPARE(primaryService->characteristics().count(), 0); |
| 687 | const QList<QBluetoothUuid> includedServices = primaryService->includedServices(); |
| 688 | QCOMPARE(includedServices.count(), 1); |
| 689 | QCOMPARE(includedServices.first(), secondaryService->serviceUuid()); |
| 690 | } |
| 691 | |
| 692 | QTEST_MAIN(TestQLowEnergyControllerGattServer) |
| 693 | |
| 694 | #include "tst_qlowenergycontroller-gattserver.moc" |
| 695 | |