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 | |