diff options
Diffstat (limited to 'tests/manual/qlowenergycontroller/tst_qlowenergycontroller_device.cpp')
-rw-r--r-- | tests/manual/qlowenergycontroller/tst_qlowenergycontroller_device.cpp | 945 |
1 files changed, 945 insertions, 0 deletions
diff --git a/tests/manual/qlowenergycontroller/tst_qlowenergycontroller_device.cpp b/tests/manual/qlowenergycontroller/tst_qlowenergycontroller_device.cpp new file mode 100644 index 00000000..e492363a --- /dev/null +++ b/tests/manual/qlowenergycontroller/tst_qlowenergycontroller_device.cpp @@ -0,0 +1,945 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QObject> +#include <QtGlobal> +#include <QTest> +#include <QBluetoothAddress> +#include <QBluetoothDeviceDiscoveryAgent> +#include <QSignalSpy> +#include <QLowEnergyController> +#include <QLoggingCategory> +#include <QScopeGuard> +#include <QBluetoothLocalDevice> + +#if QT_CONFIG(permissions) +#include <QtCore/qcoreapplication.h> +#include <QtCore/qpermissions.h> +#include <QtCore/qnamespace.h> +#endif + +static const QLatin1String largeAttributeServiceUuid("1f85e37c-ac16-11eb-ae5c-93d3a763feed"); +static const QLatin1String largeAttributeCharUuid("40e4f68e-ac16-11eb-9956-cfe55a8c370c"); +static const QLatin1String largeAttributeDescUuid("44e4f68e-ac16-11eb-9956-cfe55a8c370c"); +static constexpr qsizetype largeAttributeSize{508}; // Size for char and desc values + +static const QLatin1String platformIdentifierServiceUuid("4a92cb7f-5031-4a09-8304-3e89413f458d"); +static const QLatin1String platformIdentifierCharUuid("6b0ecf7c-5f09-4c87-aaab-bb49d5d383aa"); + +static const QLatin1String + notificationIndicationTestServiceUuid("bb137ac5-5716-4b80-873b-e2d11d29efe2"); +static const QLatin1String + notificationIndicationTestChar1Uuid("6da4d652-0248-478a-a5a8-1e2f076158cc"); +static const QLatin1String + notificationIndicationTestChar2Uuid("990930f0-b9cc-4c27-8c1b-ebc2bcae5c95"); +static const QLatin1String + notificationIndicationTestChar3Uuid("9a60486b-de5b-4e03-b914-4e158c0bd388"); +static const QLatin1String + notificationIndicationTestChar4Uuid("d92435d4-6c2e-43f8-a6be-bbb66b5a3e28"); + +static const QLatin1String connectionCountServiceUuid("78c61a07-a0f9-4b92-be2d-2570d8dbf010"); +static const QLatin1String connectionCountCharUuid("9414ec2d-792f-46a2-a19e-186d0fb38a08"); + +static const QLatin1String repeatedWriteServiceUuid("72b12a31-98ea-406d-a89d-2c932d11ff67"); +static const QLatin1String repeatedWriteTargetCharUuid("2192ee43-6d17-4e78-b286-db2c3b696833"); +static const QLatin1String repeatedWriteNotifyCharUuid("b3f9d1a2-3d55-49c9-8b29-e09cec77ff86"); + + +// With the defines below some test cases are picked on at compile-time as opposed to runtime skips +// to avoid the lengthy init() and clean() executions +#if defined(QT_ANDROID_BLUETOOTH) || defined(QT_WINRT_BLUETOOTH) || defined(Q_OS_DARWIN) +#define QT_BLUETOOTH_MTU_SUPPORTED +#endif + +#if defined(QT_ANDROID_BLUETOOTH) || defined(Q_OS_DARWIN) +#define QT_BLUETOOTH_RSSI_SUPPORTED +#endif + +#if defined(QT_BLUETOOTH_MTU_SUPPORTED) +static const QLatin1String mtuServiceUuid("9a9483eb-cf4f-4c32-9a6b-794238d5b483"); +static const QLatin1String mtuCharUuid("960d7e2a-a850-4a70-8064-cd74e9ccb6ff"); +#endif + +/* + * This class contains all unit tests for QLowEnergyController + * which require a remote device and can thus not run completely automated. + * + * This testcase acts as a bluetooth LE *client*. + * The counterpart *server* device is the "bluetoothtestdevice" in the same + * repository and it needs to be ran when this testcase is ran. + * + * The name of the server device needs to be adjusted in this code so that + * this LE client can find it. For example on macOS and Linux it is a generic + * "BluetoothTestDevice" where as on Android it is the device's bluetooth name + * set by the user. Please see the BTLE_SERVER_DEVICE_NAME below. + */ +class tst_qlowenergycontroller_device : public QObject +{ + Q_OBJECT + +public: + tst_qlowenergycontroller_device(); + ~tst_qlowenergycontroller_device(); +private slots: + void initTestCase(); + void init(); + void cleanup(); + void cleanupTestCase(); + + // Keep readServerPlatform as the first test, later tests + // may depend on its results. + void readServerPlatform(); + +#if defined(QT_BLUETOOTH_MTU_SUPPORTED) + void checkMtuNegotiation(); +#endif +#if defined(QT_BLUETOOTH_RSSI_SUPPORTED) + void rssiRead(); +#endif + void readWriteLargeCharacteristic(); + void readWriteLargeDescriptor(); + void readDuringServiceDiscovery(); + void readNotificationAndIndicationProperty(); + void testNotificationAndIndication(); + void testRepeatedCharacteristicsWrite(); + +public: + void checkconnectionCounter(std::unique_ptr<QLowEnergyController> &control); + static int connectionCounter; + +private: + void discoverTestServer(); + + std::unique_ptr<QBluetoothDeviceDiscoveryAgent> mDevAgent; + std::unique_ptr<QLowEnergyController> mController; + QBluetoothDeviceInfo mRemoteDeviceInfo; + QString mServerDeviceName; + QByteArray mServerPlatform; +#if QT_CONFIG(permissions) + Qt::PermissionStatus permissionStatus = Qt::PermissionStatus::Undetermined; +#endif +}; + +// connectionCounter is used to check that the server-side connect events +// occur as expected. On the first time when the value is the initial "-1" +// we read the current connection count from a service/characteristic providing it. +// This way we don't need to always restart the server-side for testing +int tst_qlowenergycontroller_device::connectionCounter = -1; + +tst_qlowenergycontroller_device::tst_qlowenergycontroller_device() +{ + QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); +#if QT_CONFIG(permissions) + // FIXME: Android has more specific BT permissions, fix when appropriate + // change is done in qtbase + permissionStatus = qApp->checkPermission(QBluetoothPermission{}); + if (qApp->checkPermission(QBluetoothPermission{}) == Qt::PermissionStatus::Undetermined) { + QTestEventLoop loop; + qApp->requestPermission(QBluetoothPermission{}, [this, &loop](const QPermission &permission){ + permissionStatus = permission.status(); + loop.exitLoop(); + }); + if (permissionStatus == Qt::PermissionStatus::Undetermined) + loop.enterLoopMSecs(30000); + } +#endif +} + +tst_qlowenergycontroller_device::~tst_qlowenergycontroller_device() { } + +void tst_qlowenergycontroller_device::initTestCase() +{ +#if QT_CONFIG(permissions) + if (permissionStatus != Qt::PermissionStatus::Granted) + QSKIP("This manual test requires Blutooth permissions granted."); +#endif + qDebug() << "Testcase build time: " << __TIME__; + mDevAgent.reset(new QBluetoothDeviceDiscoveryAgent(this)); + mDevAgent->setLowEnergyDiscoveryTimeout(75000); + mServerDeviceName = qEnvironmentVariable("BTLE_SERVER_DEVICE_NAME"); + if (mServerDeviceName.isEmpty()) + mServerDeviceName = QStringLiteral("BluetoothTestDevice"); + qDebug() << "Using server device name for testing: " << mServerDeviceName; + qDebug() << "To change this set BTLE_SERVER_DEVICE_NAME environment variable"; +} + +void tst_qlowenergycontroller_device::discoverTestServer() +{ + mRemoteDeviceInfo = QBluetoothDeviceInfo(); // Invalidate whatever we had before + QSignalSpy finishedSpy(mDevAgent.get(), SIGNAL(finished())); + QSignalSpy canceledSpy(mDevAgent.get(), SIGNAL(canceled())); + // there should be no changes yet + QVERIFY(finishedSpy.isValid() && finishedSpy.isEmpty()); + QVERIFY(canceledSpy.isValid() && canceledSpy.isEmpty()); + + QObject forLifeTime; + QObject::connect(mDevAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + &forLifeTime, [&](const QBluetoothDeviceInfo& info) { + if (info.name() == mServerDeviceName) { + qDebug() << "Matching server device discovered, stopping device discovery agent"; + mDevAgent->stop(); + } + }); + + // Start device discovery + mDevAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + QTRY_VERIFY_WITH_TIMEOUT(!finishedSpy.isEmpty() || !canceledSpy.isEmpty(), 80000); + + // Verify that we have found a matching server device + bool deviceFound = false; + const QList<QBluetoothDeviceInfo> infos = mDevAgent->discoveredDevices(); + for (const QBluetoothDeviceInfo &info : infos) { + if (info.name() == mServerDeviceName) { + mRemoteDeviceInfo = info; + deviceFound = true; + qDebug() << "Found a matching server device" << info.name() << info.address(); + break; + } + qDebug() << "Ignoring a non-matching device:" << info.name() << info.address(); + } + QVERIFY2(deviceFound, "Cannot find remote device."); +} + + +void tst_qlowenergycontroller_device::init() +{ + // Connect to the test server. If unsuccessful, rediscover and retry. + // Rediscovery may be needed as the address of the remote can change and + // also at least Bluez backend recovers from its own internal errors + // more likely making the test less flaky + while (!mController || mController->state() != QLowEnergyController::ConnectedState) { + discoverTestServer(); + QVERIFY(mRemoteDeviceInfo.isValid()); + mController.reset(QLowEnergyController::createCentral(mRemoteDeviceInfo)); + mController->connectToDevice(); + QTRY_VERIFY_WITH_TIMEOUT(mController->state() != QLowEnergyController::ConnectingState, 45000); + if (mController->state() != QLowEnergyController::ConnectedState) { + mController.reset(); + qDebug() << "Retrying connecting to the server in 5 seconds"; + QTest::qWait(5000); + } + } + QCOMPARE(mController->state(), QLowEnergyController::ConnectedState); + QCOMPARE(mController->error(), QLowEnergyController::NoError); +} + +void tst_qlowenergycontroller_device::cleanup() +{ + mController->disconnectFromDevice(); + + // Attempt a graceful close. If the test has already failed, the QTRY would exit immediately + if (!QTest::currentTestFailed()) + QTRY_VERIFY_WITH_TIMEOUT(mController->state() == + QLowEnergyController::UnconnectedState, 30000); + else + QTest::qWait(2000); + + QCOMPARE(mController->state(), QLowEnergyController::UnconnectedState); + qDebug() << "Disconnected from remote device, waiting 5s before deleting controller."; + QTest::qWait(5000); + mController.reset(); +} + +void tst_qlowenergycontroller_device::cleanupTestCase() { } + +// The readServerPlatform should always be the first actual test function to execute. +// It reads a characteristic where the server reports on which platform it runs on. +// This information can then be used to adjust the test case's behavior accordingly; +// different platforms support slightly different things. +void tst_qlowenergycontroller_device::readServerPlatform() +{ + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + QCOMPARE(mController->error(), QLowEnergyController::NoError); + + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(platformIdentifierServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + auto characteristic = service->characteristic(QBluetoothUuid(platformIdentifierCharUuid)); + mServerPlatform = characteristic.value(); + qDebug() << "Server reported its running on: " << mServerPlatform; + QVERIFY(!mServerPlatform.isEmpty()); +} + + +#if defined(QT_BLUETOOTH_MTU_SUPPORTED) +void tst_qlowenergycontroller_device::checkMtuNegotiation() +{ + // service discovery, including MTU negotiation + qDebug() << "Client-side MTU after connect" << mController->mtu(); + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + QCOMPARE(mController->error(), QLowEnergyController::NoError); + + checkconnectionCounter(mController); + + // now a larger MTU should have been negotiated + QTRY_VERIFY(mController->mtu() > 23); + qDebug() << "MTU after service discovery" << mController->mtu(); + + // check that central and peripheral agree on negotiated mtu + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(mtuServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + auto characteristic = service->characteristic(QBluetoothUuid(mtuCharUuid)); + int mtu; + memcpy(&mtu, characteristic.value().constData(), sizeof(int)); + qDebug() << "MTU reported by server-side:" << mtu; + // Server-side mtu is not supported on all platforms + if (mServerPlatform != "linux" && mServerPlatform != "darwin") + QCOMPARE(mtu, mController->mtu()); +} +#endif +#undef QT_BLUETOOTH_MTU_SUPPORTED + +#if defined(QT_BLUETOOTH_RSSI_SUPPORTED) + +#define READ_AND_VERIFY_RSSI_VALUE \ + errorSpy.clear(); \ + rssiSpy.clear(); \ + mController->readRssi(); \ + QTRY_VERIFY(!rssiSpy.isEmpty()); \ + QVERIFY(errorSpy.isEmpty()); \ + rssi = rssiSpy.takeFirst().at(0).toInt(); \ + QCOMPARE_GE(rssi, -127); \ + QCOMPARE_LE(rssi, 127); \ + +void tst_qlowenergycontroller_device::rssiRead() +{ + QSignalSpy errorSpy(mController.get(), &QLowEnergyController::errorOccurred); + QSignalSpy rssiSpy(mController.get(), &QLowEnergyController::rssiRead); + int rssi = 1000; // valid values are -127..127 + + // 1) Read RSSI in connected state + QCOMPARE(mController->state(), QLowEnergyController::ConnectedState); + READ_AND_VERIFY_RSSI_VALUE; + // Check that the value changes. This might seem flaky with stationary bluetooth + // devices but in practice the RSSI constantly fluctuates + const int initialRssi = rssi; + for (int attempt = 1; attempt < 20; attempt++) { + READ_AND_VERIFY_RSSI_VALUE; + QTest::qWait(200); // Provide a bit time for the RSSI to change + if (rssi != initialRssi) + break; + } + QVERIFY(rssi != initialRssi); + + // 2) Read RSSI while discovering services + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QCOMPARE(mController->state(), QLowEnergyController::DiscoveringState); + READ_AND_VERIFY_RSSI_VALUE; + + // 3) Read RSSI after services have been discovered + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + READ_AND_VERIFY_RSSI_VALUE; + + // 4) Read RSSI while discovering service details + QSharedPointer<QLowEnergyService> service(mController->createServiceObject( + QBluetoothUuid(repeatedWriteServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QCOMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovering); + READ_AND_VERIFY_RSSI_VALUE; + + // 5) Read RSSI after service detail discovery + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + READ_AND_VERIFY_RSSI_VALUE; + + checkconnectionCounter(mController); + + // 6) Read RSSI amidst characteristic reads and writes + QSignalSpy writeSpy(service.get(), &QLowEnergyService::characteristicWritten); + QSignalSpy readSpy(service.get(), &QLowEnergyService::characteristicRead); + auto characteristic = service->characteristic(QBluetoothUuid(repeatedWriteTargetCharUuid)); + QByteArray value(8, 'a'); + service->writeCharacteristic(characteristic, value); + READ_AND_VERIFY_RSSI_VALUE; + QTRY_VERIFY(!writeSpy.isEmpty()); + + service->readCharacteristic(characteristic); + READ_AND_VERIFY_RSSI_VALUE; + QTRY_VERIFY(!readSpy.isEmpty()); + + // A bit of stress-testing to check that read/write and + // RSSI reading don't interfere with one another + writeSpy.clear(); + readSpy.clear(); + rssiSpy.clear(); + errorSpy.clear(); + const int TIMES = 5; + for (int i = 0; i < TIMES; i++) { + mController->readRssi(); + service->writeCharacteristic(characteristic, value); + mController->readRssi(); + service->readCharacteristic(characteristic); + } + QTRY_COMPARE(writeSpy.size(), TIMES); + QTRY_COMPARE(readSpy.size(), TIMES); +#if defined(Q_OS_ANDROID) + QTRY_COMPARE(rssiSpy.size(), TIMES * 2); +#else + // On darwin several pending requests will get one callback + QTRY_COMPARE_GE(rssiSpy.size(), 1); +#endif + QVERIFY(errorSpy.isEmpty()); + + // 7) Disconnect the device and verify we get error for RSSI read + errorSpy.clear(); + rssiSpy.clear(); + mController->disconnectFromDevice(); + // First right after requesting disconnect + mController->readRssi(); + QTRY_COMPARE(errorSpy.size(), 1); + QCOMPARE(errorSpy.takeFirst().at(0).value<QLowEnergyController::Error>(), + QLowEnergyController::Error::RssiReadError); + QCOMPARE(mController->error(), QLowEnergyController::Error::RssiReadError); + QVERIFY(rssiSpy.isEmpty()); + + // Then once the disconnection is complete + QTRY_COMPARE(mController->state(), QLowEnergyController::UnconnectedState); + errorSpy.clear(); + mController->readRssi(); + QTRY_COMPARE(errorSpy.size(), 1); + QCOMPARE(errorSpy.takeFirst().at(0).value<QLowEnergyController::Error>(), + QLowEnergyController::Error::RssiReadError); + QCOMPARE(mController->error(), QLowEnergyController::Error::RssiReadError); + QVERIFY(rssiSpy.isEmpty()); +} +#endif +#undef QT_BLUETOOTH_RSSI_SUPPORTED + +void tst_qlowenergycontroller_device::checkconnectionCounter( + std::unique_ptr<QLowEnergyController> &mController) +{ + // on darwin the server-side connect/disconnect events are not reported reliably + if (mServerPlatform == "darwin") + return; + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(connectionCountServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + auto counterCharacteristic = service->characteristic(QBluetoothUuid(connectionCountCharUuid)); + // If we have just started the test, read the current connection counter from the server. + // The connection counter is essentially the number of "connected" events the server + // has had. + if (connectionCounter == -1) { + memcpy(&connectionCounter, counterCharacteristic.value().constData(), sizeof(int)); + qDebug() << "Connection counter initialized from the server to:" << connectionCounter; + } else { + connectionCounter++; + QByteArray value((const char *)&connectionCounter, sizeof(int)); + QCOMPARE(counterCharacteristic.value(), value); + } +} + +void tst_qlowenergycontroller_device::readWriteLargeCharacteristic() +{ + // This tests reading and writing a large characteristic value + // + // Most modern platforms cope with up-to 512 bytes. One exception is Bluez DBus peripheral, + // which may reject a write from client when going above 508 due to the way it internally + // checks the payload size (tested with Bluez 5.64). + // + // This test can also be used for testing long (aka prepared) writes & reads by setting + // the MTU to for example 50 bytes. Asking a specific MTU is platform-specific and + // there is no Qt API for it + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(largeAttributeServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::SkipValueDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + QSignalSpy readSpy(service.get(), &QLowEnergyService::characteristicRead); + QSignalSpy writtenSpy(service.get(), &QLowEnergyService::characteristicWritten); + + // The service discovery skipped the values => check that the default values are all zeroes + auto characteristic = service->characteristic(QBluetoothUuid(largeAttributeCharUuid)); + QVERIFY(characteristic.isValid()); + QByteArray testArray(0); + qDebug() << "Initial large characteristic value:" << characteristic.value(); + QCOMPARE(characteristic.value(), testArray); + + // Read the characteristic value and verify it is the one the server-side sets (0x0b 0x00 ..) + service->readCharacteristic(characteristic); + QTRY_COMPARE(readSpy.size(), 1); + qDebug() << "Large characteristic value after read:" << characteristic.value(); + testArray = QByteArray(largeAttributeSize, 0); + testArray[0] = 0x0b; + QCOMPARE(characteristic.value(), testArray); + + // Write a new value to characteristic and read it back + for (int i = 0; i < largeAttributeSize; ++i) { + testArray[i] = i % 5; + } + service->writeCharacteristic(characteristic, testArray); + QCOMPARE(service->error(), QLowEnergyService::ServiceError::NoError); + QTRY_COMPARE(writtenSpy.size(), 1); + + service->readCharacteristic(characteristic); + QTRY_COMPARE(readSpy.size(), 2); + qDebug() << "Large characteristic value after write/read:" << characteristic.value(); + QCOMPARE(characteristic.value(), testArray); +} + +void tst_qlowenergycontroller_device::readWriteLargeDescriptor() +{ + // This tests reading and writing a large descriptor value + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(largeAttributeServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + QSignalSpy readSpy(service.get(), &QLowEnergyService::descriptorRead); + QSignalSpy writtenSpy(service.get(), &QLowEnergyService::descriptorWritten); + + auto characteristic = service->characteristic(QBluetoothUuid(largeAttributeCharUuid)); + QVERIFY(characteristic.isValid()); + auto descriptor = characteristic.descriptor(QBluetoothUuid(largeAttributeDescUuid)); + QVERIFY(descriptor.isValid()); + + QByteArray testArray = QByteArray(largeAttributeSize, 0); + testArray[0] = 0xdd; + + // Read descriptor value and verify it is what the server set (0xdd 0x00 ..) + QVERIFY(readSpy.isEmpty()); + QVERIFY(writtenSpy.isEmpty()); + service->readDescriptor(descriptor); + QTRY_COMPARE(readSpy.size(), 1); + qDebug() << "Large descriptor value after read:" << descriptor.value(); + QCOMPARE(descriptor.value(), testArray); + + // Write a new value to descriptor and read it back + for (int i = 0; i < largeAttributeSize; ++i) { + testArray[i] = i % 5; + } + service->writeDescriptor(descriptor, testArray); + QCOMPARE(service->error(), QLowEnergyService::ServiceError::NoError); + QTRY_COMPARE(writtenSpy.size(), 1); + + service->readDescriptor(descriptor); + QTRY_COMPARE(readSpy.size(), 2); + qDebug() << "Large descriptor value after write/read:" << descriptor.value(); + QCOMPARE(descriptor.value(), testArray); +} + +void tst_qlowenergycontroller_device::readDuringServiceDiscovery() +{ + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + QSharedPointer<QLowEnergyService> service( + mController->createServiceObject(QBluetoothUuid(largeAttributeServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + QSignalSpy readSpy(service.get(), &QLowEnergyService::characteristicRead); + QSignalSpy writtenSpy(service.get(), &QLowEnergyService::characteristicWritten); + QCOMPARE(readSpy.size(), 0); + QCOMPARE(writtenSpy.size(), 0); + + // Value that is initially set on the characteristic at the server-side (0x0b 0x00 ..) + QByteArray testArray(largeAttributeSize, 0); + testArray[0] = 0x0b; + + // We did a full service discovery and should have an initial value to compare + auto characteristic = service->characteristic(QBluetoothUuid(largeAttributeCharUuid)); + QByteArray valueFromServiceDiscovery = characteristic.value(); + qDebug() << "Large characteristic value from service discovery:" << valueFromServiceDiscovery; + // On darwin the server does not restart (disconnect) in-between the case runs + // and the initial value may be from a previous test function + if (mServerPlatform != "darwin") + QCOMPARE(characteristic.value(), testArray); + + // Check that the value from service discovery and explicit characteristic read match + service->readCharacteristic(characteristic); + QTRY_COMPARE(readSpy.size(), 1); + qDebug() << "Large characteristic value after read:" << characteristic.value(); + QCOMPARE(characteristic.value(), valueFromServiceDiscovery); + + // Write a new value to the characteristic and read it back + for (int i = 0; i < largeAttributeSize; ++i) { + testArray[i] = i % 5; + } + service->writeCharacteristic(characteristic, testArray); + QCOMPARE(service->error(), QLowEnergyService::ServiceError::NoError); + QTRY_COMPARE(writtenSpy.size(), 1); + + service->readCharacteristic(characteristic); + QTRY_COMPARE(readSpy.size(), 2); + qDebug() << "Large characteristic value after write/read:" << characteristic.value(); + QCOMPARE(characteristic.value(), testArray); +} + +void tst_qlowenergycontroller_device::readNotificationAndIndicationProperty() +{ + // discover services + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + // check test service is available + QVERIFY(mController->services().contains(QBluetoothUuid(notificationIndicationTestServiceUuid))); + + // get service object + QSharedPointer<QLowEnergyService> service(mController->createServiceObject( + QBluetoothUuid(notificationIndicationTestServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + // check that all four characteristics are found + QCOMPARE(service->characteristics().size(), 4); + + // check that properties are correctly set + auto notifyOrIndicate = QLowEnergyCharacteristic::PropertyType::Notify + | QLowEnergyCharacteristic::PropertyType::Indicate; + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar1Uuid)); + QCOMPARE(characteristic.properties() & notifyOrIndicate, 0); + } + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar2Uuid)); + QCOMPARE(characteristic.properties() & notifyOrIndicate, + QLowEnergyCharacteristic::PropertyType::Notify); + } + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar3Uuid)); + QCOMPARE(characteristic.properties() & notifyOrIndicate, + QLowEnergyCharacteristic::PropertyType::Indicate); + } + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar4Uuid)); + QCOMPARE(characteristic.properties() & notifyOrIndicate, notifyOrIndicate); + } +} + +void tst_qlowenergycontroller_device::testNotificationAndIndication() +{ + // discover services + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + // get service object + QSharedPointer<QLowEnergyService> service(mController->createServiceObject( + QBluetoothUuid(notificationIndicationTestServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + // Verify that notification works + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar2Uuid)); + auto notifyOrIndicate = QLowEnergyCharacteristic::PropertyType::Notify + | QLowEnergyCharacteristic::PropertyType::Indicate; + QCOMPARE(characteristic.properties() & notifyOrIndicate, + QLowEnergyCharacteristic::PropertyType::Notify); + + // getting cccd + QLowEnergyDescriptor cccd = characteristic.clientCharacteristicConfiguration(); + QVERIFY(cccd.isValid()); + + // write to cccd + bool cccdWritten = false; + QObject dummy; // for lifetime management + QObject::connect( + service.get(), &QLowEnergyService::descriptorWritten, &dummy, + [&cccdWritten](const QLowEnergyDescriptor &info, const QByteArray &) { + if (info.uuid() + == QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) { + cccdWritten = true; + } + }); + service->writeDescriptor(cccd, QLowEnergyCharacteristic::CCCDEnableNotification); + QTRY_VERIFY(cccdWritten); + + // notification should be enabled + auto oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + // wait again + oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + + // disable notification + cccdWritten = false; + service->writeDescriptor(cccd, QLowEnergyCharacteristic::CCCDDisable); + QTRY_VERIFY(cccdWritten); + + // wait for a moment in case there is a value change just happening, + // then check that there are no more notifications: + QTest::qWait(200); + oldvalue = characteristic.value(); + for (int i = 0; i < 3; ++i) { + QTest::qWait(100); + QCOMPARE(characteristic.value(), oldvalue); + } + } + + // Verify that indication works + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar3Uuid)); + auto notifyOrIndicate = QLowEnergyCharacteristic::PropertyType::Notify + | QLowEnergyCharacteristic::PropertyType::Indicate; + QCOMPARE(characteristic.properties() & notifyOrIndicate, + QLowEnergyCharacteristic::PropertyType::Indicate); + + // getting cccd + QLowEnergyDescriptor cccd = characteristic.clientCharacteristicConfiguration(); + QVERIFY(cccd.isValid()); + + // write to cccd + bool cccdWritten = false; + QObject dummy; // for lifetime management + QObject::connect( + service.get(), &QLowEnergyService::descriptorWritten, &dummy, + [&cccdWritten](const QLowEnergyDescriptor &info, const QByteArray &) { + if (info.uuid() + == QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) { + cccdWritten = true; + } + }); + QByteArray newValue = QByteArray::fromHex("0200"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // indication should be enabled + auto oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + // wait again + oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + + // disable indication + cccdWritten = false; + newValue = QByteArray::fromHex("0000"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // wait for a moment in case there is a value change just happening, + // then check that there are no more notifications: + QTest::qWait(200); + oldvalue = characteristic.value(); + for (int i = 0; i < 3; ++i) { + QTest::qWait(100); + QCOMPARE(characteristic.value(), oldvalue); + } + } + + // indication and notification works +#if defined(Q_OS_LINUX) + // If the client (this testcase) is linux and the characteristic has both + // NTF & IND supported, the Bluez stack will try to enable both NTF & IND + // at the same time regardless of what we write to CCCD. Darwin server will + // reject this as an error (ATT error 0xf5, which is a reserved error) + if (mServerPlatform != "darwin") +#endif + { + QLowEnergyCharacteristic characteristic = + service->characteristic(QBluetoothUuid(notificationIndicationTestChar4Uuid)); + auto notifyOrIndicate = QLowEnergyCharacteristic::PropertyType::Notify + | QLowEnergyCharacteristic::PropertyType::Indicate; + QCOMPARE(characteristic.properties() & notifyOrIndicate, notifyOrIndicate); + + // getting cccd + QLowEnergyDescriptor cccd = characteristic.clientCharacteristicConfiguration(); + QVERIFY(cccd.isValid()); + + // write to cccd + bool cccdWritten = false; + QObject dummy; // for lifetime management + QObject::connect( + service.get(), &QLowEnergyService::descriptorWritten, &dummy, + [&cccdWritten](const QLowEnergyDescriptor &info, const QByteArray &) { + if (info.uuid() + == QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) { + cccdWritten = true; + } + }); + QByteArray newValue = QByteArray::fromHex("0100"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // notification should be enabled + auto oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + // wait again + oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + + // disable notification + cccdWritten = false; + newValue = QByteArray::fromHex("0000"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // wait for a moment in case there is a value change just happening, + // then check that there are no more notifications: + QTest::qWait(200); + oldvalue = characteristic.value(); + for (int i = 0; i < 3; ++i) { + QTest::qWait(100); + QCOMPARE(characteristic.value(), oldvalue); + } + + newValue = QByteArray::fromHex("0200"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // indication should be enabled + oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + // wait again + oldvalue = characteristic.value(); + QCOMPARE(characteristic.value(), oldvalue); + // wait for change + QTRY_VERIFY(characteristic.value() != oldvalue); + + // disable indication + cccdWritten = false; + newValue = QByteArray::fromHex("0000"); + service->writeDescriptor(cccd, newValue); + QTRY_VERIFY(cccdWritten); + + // wait for a moment in case there is a value change just happening, + // then check that there are no more indications: + QTest::qWait(200); + oldvalue = characteristic.value(); + for (int i = 0; i < 3; ++i) { + QTest::qWait(100); + QCOMPARE(characteristic.value(), oldvalue); + } + } +} + +void tst_qlowenergycontroller_device::testRepeatedCharacteristicsWrite() +{ + // This test generates multiple consecutive writes to the same characteristic + // and waits for the notifications (on other characteristic) with the same + // values. After that it verifies that the received values are the same (and + // in the same order) as written values. The server writes each received + // value to a notifying characteristic, which allows us to perform the check. + + // Discover services + QVERIFY(mController->services().isEmpty()); + mController->discoverServices(); + QTRY_COMPARE(mController->state(), QLowEnergyController::DiscoveredState); + + checkconnectionCounter(mController); + + // Get service object. + QSharedPointer<QLowEnergyService> service(mController->createServiceObject( + QBluetoothUuid(repeatedWriteServiceUuid))); + QVERIFY(service != nullptr); + service->discoverDetails(QLowEnergyService::FullDiscovery); + QTRY_COMPARE(service->state(), QLowEnergyService::ServiceState::RemoteServiceDiscovered); + + // Enable notification. + QLowEnergyCharacteristic notifyChar = + service->characteristic(QBluetoothUuid(repeatedWriteNotifyCharUuid)); + const auto notifyOrIndicate = QLowEnergyCharacteristic::PropertyType::Notify + | QLowEnergyCharacteristic::PropertyType::Indicate; + QCOMPARE(notifyChar.properties() & notifyOrIndicate, + QLowEnergyCharacteristic::PropertyType::Notify); + + QLowEnergyDescriptor cccd = notifyChar.clientCharacteristicConfiguration(); + QVERIFY(cccd.isValid()); + + QObject dummy; // for lifetime management + bool cccdWritten = false; + connect(service.get(), &QLowEnergyService::descriptorWritten, &dummy, + [&cccdWritten](const QLowEnergyDescriptor &info, const QByteArray &) { + if (info.uuid() + == QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration) { + cccdWritten = true; + } + }); + service->writeDescriptor(cccd, QLowEnergyCharacteristic::CCCDEnableNotification); + QTRY_VERIFY(cccdWritten); + + // Track the notifications of value changes. + QList<QByteArray> receivedValues; + connect(service.get(), &QLowEnergyService::characteristicChanged, &dummy, + [&receivedValues](const QLowEnergyCharacteristic &characteristic, + const QByteArray &value) + { + if (characteristic.uuid() == QBluetoothUuid(repeatedWriteNotifyCharUuid)) { + receivedValues.push_back(value); + } + }); + + // Write characteristics multiple times. This shouldn't crash, and all + // values should be written. We use the notifications to track it. + receivedValues.clear(); + QList<QByteArray> sentValues; + QLowEnergyCharacteristic writeChar = + service->characteristic(QBluetoothUuid(repeatedWriteTargetCharUuid)); + static const int totalWrites = 50; + QByteArray value(8, 0); + for (int i = 0; i < totalWrites; ++i) { + value[0] += 1; + value[7] += 1; + service->writeCharacteristic(writeChar, value); + sentValues.push_back(value); + } + + // We expect to get notifications about all writes. + // We set a large timeout to be on a safe side. + QTRY_COMPARE_WITH_TIMEOUT(receivedValues.size(), totalWrites, 60000); + QCOMPARE(receivedValues, sentValues); +} + +QTEST_MAIN(tst_qlowenergycontroller_device) + +#include "tst_qlowenergycontroller_device.moc" |