// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #if QT_CONFIG(bluez) #include #endif #include #include #include #include #include #include #if QT_CONFIG(permissions) #include #include #include #endif #include /*! This test requires a TI sensor tag with Firmware version: 1.5 (Oct 23 2013). Since revision updates change user strings and even shift handles around other versions than the above are unlikely to succeed. Please update the sensor tag before continuing. The TI sensor can be updated using the related iOS app. The Android version doesn't seem to update at this point in time. */ QT_USE_NAMESPACE class tst_QLowEnergyController : public QObject { Q_OBJECT public: tst_QLowEnergyController(); ~tst_QLowEnergyController(); private slots: void initTestCase(); void init(); void cleanupTestCase(); void tst_emptyCtor(); void tst_connect(); void tst_concurrentDiscovery(); void tst_defaultBehavior(); void tst_writeCharacteristic(); void tst_writeCharacteristicNoResponse(); void tst_readWriteDescriptor(); void tst_customProgrammableDevice(); void tst_errorCases(); void tst_rssiError(); private: void verifyServiceProperties(const QLowEnergyService *info); bool verifyClientCharacteristicValue(const QByteArray& value); QBluetoothDeviceDiscoveryAgent *devAgent; QBluetoothAddress remoteDevice; QBluetoothDeviceInfo remoteDeviceInfo; QList foundServices; bool isBluezDbusLE = false; #if QT_CONFIG(permissions) Qt::PermissionStatus permissionStatus = Qt::PermissionStatus::Undetermined; #endif }; tst_QLowEnergyController::tst_QLowEnergyController() { //QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); #ifndef Q_OS_MAC // Core Bluetooth (OS X and iOS) does not work with addresses, // making the code below useless. const QString remote = qgetenv("BT_TEST_DEVICE"); if (!remote.isEmpty()) { remoteDevice = QBluetoothAddress(remote); qWarning() << "Using remote device " << remote << " for testing. Ensure that the device is discoverable for pairing requests"; } else { qWarning() << "Not using any remote device for testing. Set BT_TEST_DEVICE env to run manual tests involving a remote device"; } #endif #if QT_CONFIG(bluez) // This debug is needed to determine runtime configuration in the Qt CI. isBluezDbusLE = (bluetoothdVersion() >= QVersionNumber(5, 42)); qDebug() << "isDBusBluez:" << isBluezDbusLE; #endif #if QT_CONFIG(permissions) // FIXME: for Android, set additional parameters for scan and connect // permissions. permissionStatus = qApp->checkPermission(QBluetoothPermission{}); // Note: even with missing Bluetooth permission, we still can run tests on // LE controller to test its logic/errors it emits, even if we cannot scan // and cannot connect. const bool ciRun = qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci"); if (!ciRun && permissionStatus == 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 // QT_CONFIG(permissions) } tst_QLowEnergyController::~tst_QLowEnergyController() { } void tst_QLowEnergyController::initTestCase() { #if !defined(Q_OS_MAC) if (remoteDevice.isNull() #if !QT_CONFIG(winrt_bt) || QBluetoothLocalDevice::allDevices().isEmpty()) { #else ) { #endif qWarning("No remote device or local adapter found."); return; } #elif defined(Q_OS_MACOS) // allDevices is always empty on iOS: if (QBluetoothLocalDevice::allDevices().isEmpty()) { qWarning("No local adapter found."); return; } #endif // QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); devAgent = new QBluetoothDeviceDiscoveryAgent(this); devAgent->setLowEnergyDiscoveryTimeout(5000); QSignalSpy finishedSpy(devAgent, SIGNAL(finished())); // there should be no changes yet QVERIFY(finishedSpy.isValid()); QVERIFY(finishedSpy.isEmpty()); bool deviceFound = false; devAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); #if QT_CONFIG(permissions) if (permissionStatus != Qt::PermissionStatus::Granted) { QCOMPARE(devAgent->error(), QBluetoothDeviceDiscoveryAgent::MissingPermissionsError); return; } #endif QTRY_VERIFY_WITH_TIMEOUT(!finishedSpy.isEmpty(), 30000); const QList infos = devAgent->discoveredDevices(); for (const QBluetoothDeviceInfo &info : infos) { #ifndef Q_OS_MAC if (info.address() == remoteDevice) { #else // On OS X/iOS the only way to find the device we are // interested in - is to use device's name. if (info.name().contains("Sensor") && info.name().contains("Tag")) { #endif remoteDeviceInfo = info; deviceFound = true; break; } } if (!deviceFound || !remoteDeviceInfo.isValid()) qWarning() << "The sensor tag device was not found, will skip most of the test"; // These are the services exported by the TI SensorTag #ifndef Q_OS_MAC // Core Bluetooth somehow ignores/hides/fails to discover these services. if (!isBluezDbusLE) // Bluez LE Dbus intentionally hides 0x1800 service foundServices << QBluetoothUuid(QString("00001800-0000-1000-8000-00805f9b34fb")); foundServices << QBluetoothUuid(QString("00001801-0000-1000-8000-00805f9b34fb")); #endif foundServices << QBluetoothUuid(QString("0000180a-0000-1000-8000-00805f9b34fb")); foundServices << QBluetoothUuid(QString("0000ffe0-0000-1000-8000-00805f9b34fb")); foundServices << QBluetoothUuid(QString("f000aa00-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa10-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa20-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa30-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa40-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa50-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000aa60-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000ccc0-0451-4000-b000-000000000000")); foundServices << QBluetoothUuid(QString("f000ffc0-0451-4000-b000-000000000000")); } /* * Executed in between each test function call. */ void tst_QLowEnergyController::init() { #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) /* * Add a delay to give Android/iOS stack time to catch up in between * the multiple connect/disconnects within each test function. */ QTest::qWait(2000); #endif } void tst_QLowEnergyController::cleanupTestCase() { } void tst_QLowEnergyController::tst_emptyCtor() { { QScopedPointer control(QLowEnergyController::createCentral(QBluetoothDeviceInfo())); QSignalSpy connectedSpy(control.data(), SIGNAL(connected())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); QSignalSpy errorSpy(control.data(), SIGNAL(errorOccurred(QLowEnergyController::Error))); #if QT_CONFIG(bluez) QBluetoothLocalDevice localDevice; // With bluez Kernel ATT interface we get the error already at construction time if the // device does not have a bluetooth adapter if (!isBluezDbusLE && !localDevice.isValid()) QCOMPARE(control->error(), QLowEnergyController::InvalidBluetoothAdapterError); else QCOMPARE(control->error(), QLowEnergyController::NoError); #else QCOMPARE(control->error(), QLowEnergyController::NoError); #endif control->connectToDevice(); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); QVERIFY(connectedSpy.isEmpty()); QVERIFY(stateSpy.isEmpty()); QLowEnergyController::Error lastError = errorSpy[0].at(0).value(); QVERIFY(lastError == QLowEnergyController::UnknownRemoteDeviceError || lastError == QLowEnergyController::InvalidBluetoothAdapterError); } { QScopedPointer control(QLowEnergyController::createCentral( QBluetoothDeviceInfo(), QBluetoothAddress(), this)); QSignalSpy connectedSpy(control.data(), SIGNAL(connected())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); QSignalSpy errorSpy(control.data(), SIGNAL(errorOccurred(QLowEnergyController::Error))); #if QT_CONFIG(bluez) QBluetoothLocalDevice localDevice; if (!isBluezDbusLE && !localDevice.isValid()) QCOMPARE(control->error(), QLowEnergyController::InvalidBluetoothAdapterError); else QCOMPARE(control->error(), QLowEnergyController::NoError); #else QCOMPARE(control->error(), QLowEnergyController::NoError); #endif control->connectToDevice(); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); QVERIFY(connectedSpy.isEmpty()); QVERIFY(stateSpy.isEmpty()); QLowEnergyController::Error lastError = errorSpy[0].at(0).value(); QVERIFY(lastError == QLowEnergyController::UnknownRemoteDeviceError // if local device on platform found || lastError == QLowEnergyController::InvalidBluetoothAdapterError); // otherwise, e.g. fallback backend } } void tst_QLowEnergyController::tst_connect() { QList localAdapters = QBluetoothLocalDevice::allDevices(); #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || QT_CONFIG(winrt_bt) if (!remoteDeviceInfo.isValid()) #else if (localAdapters.isEmpty() || !remoteDeviceInfo.isValid()) #endif QSKIP("No local Bluetooth or remote BTLE device found. Skipping test."); QScopedPointer control(QLowEnergyController::createCentral(remoteDeviceInfo)); QCOMPARE(remoteDeviceInfo.deviceUuid(), control->remoteDeviceUuid()); QCOMPARE(control->role(), QLowEnergyController::CentralRole); QSignalSpy connectedSpy(control.data(), SIGNAL(connected())); QSignalSpy disconnectedSpy(control.data(), SIGNAL(disconnected())); if (remoteDeviceInfo.name().isEmpty()) QVERIFY(control->remoteName().isEmpty()); else QCOMPARE(control->remoteName(), remoteDeviceInfo.name()); #if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !QT_CONFIG(winrt_bt) const QBluetoothAddress localAdapter = localAdapters.at(0).address(); QCOMPARE(control->localAddress(), localAdapter); QVERIFY(!control->localAddress().isNull()); #endif #ifndef Q_OS_MAC QCOMPARE(control->remoteAddress(), remoteDevice); #endif QCOMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); QVERIFY(control->errorString().isEmpty()); QCOMPARE(disconnectedSpy.size(), 0); QCOMPARE(connectedSpy.size(), 0); QVERIFY(control->services().isEmpty()); bool wasError = false; control->connectToDevice(); QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 10000) QCOMPARE(disconnectedSpy.size(), 0); if (control->error() != QLowEnergyController::NoError) { //error during connect QCOMPARE(connectedSpy.size(), 0); QCOMPARE(control->state(), QLowEnergyController::UnconnectedState); wasError = true; } else if (control->state() == QLowEnergyController::ConnectingState) { //timeout QCOMPARE(connectedSpy.size(), 0); QVERIFY(control->errorString().isEmpty()); QCOMPARE(control->error(), QLowEnergyController::NoError); QVERIFY(control->services().isEmpty()); QSKIP("Connection to LE device cannot be established. Skipping test."); return; } else { QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QCOMPARE(connectedSpy.size(), 1); QCOMPARE(control->error(), QLowEnergyController::NoError); QVERIFY(control->errorString().isEmpty()); } QVERIFY(control->services().isEmpty()); QList savedReferences; if (!wasError) { QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy serviceFoundSpy(control.data(), SIGNAL(serviceDiscovered(QBluetoothUuid))); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); QVERIFY(!serviceFoundSpy.isEmpty()); QVERIFY(serviceFoundSpy.size() >= foundServices.size()); QVERIFY(!serviceFoundSpy.isEmpty()); QList listing; for (qsizetype i = 0; i < serviceFoundSpy.size(); ++i) { const QVariant v = serviceFoundSpy[i].at(0); listing.append(v.value()); } for (const QBluetoothUuid &uuid : std::as_const(foundServices)) { QVERIFY2(listing.contains(uuid), uuid.toString().toLatin1()); QLowEnergyService *service = control->createServiceObject(uuid); QVERIFY2(service, uuid.toString().toLatin1()); savedReferences.append(service); QCOMPARE(service->type(), QLowEnergyService::PrimaryService); QCOMPARE(service->state(), QLowEnergyService::RemoteService); } // unrelated uuids don't return valid service object // invalid service uuid QVERIFY(!control->createServiceObject(QBluetoothUuid())); // some random uuid QVERIFY(!control->createServiceObject( QBluetoothUuid(QBluetoothUuid::CharacteristicType::DeviceName))); // initiate characteristic discovery for (QLowEnergyService *service : std::as_const(savedReferences)) { qDebug() << "Discovering" << service->serviceUuid(); QSignalSpy stateSpy(service, SIGNAL(stateChanged(QLowEnergyService::ServiceState))); QSignalSpy errorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 10000); QCOMPARE(errorSpy.size(), 0); //no error QCOMPARE(stateSpy.size(), 2); // verifyServiceProperties(service); } // ensure that related service objects share same state for (QLowEnergyService* originalService : std::as_const(savedReferences)) { QLowEnergyService *newService = control->createServiceObject( originalService->serviceUuid()); QVERIFY(newService); QCOMPARE(newService->state(), QLowEnergyService::RemoteServiceDiscovered); delete newService; } } // Finish off control->disconnectFromDevice(); QTRY_VERIFY_WITH_TIMEOUT( control->state() == QLowEnergyController::UnconnectedState, 10000); if (wasError) { QCOMPARE(disconnectedSpy.size(), 0); } else { QCOMPARE(disconnectedSpy.size(), 1); // after disconnect all service references must be invalid for (const QLowEnergyService *entry : std::as_const(savedReferences)) { const QBluetoothUuid &uuid = entry->serviceUuid(); QVERIFY2(entry->state() == QLowEnergyService::InvalidService, uuid.toString().toLatin1()); //after disconnect all related characteristics and descriptors are invalid QList chars = entry->characteristics(); for (qsizetype i = 0; i < chars.size(); ++i) { QCOMPARE(chars.at(i).isValid(), false); QList descriptors = chars[i].descriptors(); for (qsizetype j = 0; j < descriptors.size(); ++j) QCOMPARE(descriptors[j].isValid(), false); } } } qDeleteAll(savedReferences); savedReferences.clear(); } void tst_QLowEnergyController::tst_concurrentDiscovery() { #if !defined(Q_OS_MACOS) && !QT_CONFIG(winrt_bt) QList localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); #endif if (!remoteDeviceInfo.isValid()) QSKIP("No remote BTLE device found. Skipping test."); QScopedPointer control(QLowEnergyController::createCentral(remoteDeviceInfo)); QCOMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); // 2. new controller to same device fails { QScopedPointer control2( QLowEnergyController::createCentral(remoteDeviceInfo)); control2->connectToDevice(); { QTRY_IMPL(control2->state() != QLowEnergyController::ConnectingState, 30000) } #if defined(Q_OS_ANDROID) || defined(Q_OS_DARWIN) || QT_CONFIG(winrt_bt) QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QCOMPARE(control2->state(), QLowEnergyController::ConnectedState); control2->disconnectFromDevice(); QTest::qWait(3000); QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QCOMPARE(control2->state(), QLowEnergyController::UnconnectedState); #else if (!isBluezDbusLE) { // see QTBUG-42519 // Linux non-DBus GATT cannot maintain two controller connections at the same time QCOMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control2->state(), QLowEnergyController::ConnectedState); control2->disconnectFromDevice(); QTRY_COMPARE(control2->state(), QLowEnergyController::UnconnectedState); QTRY_COMPARE(control2->error(), QLowEnergyController::NoError); // reconnect control control->connectToDevice(); { QTRY_VERIFY_WITH_TIMEOUT(control->state() != QLowEnergyController::ConnectingState, 30000); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); } else { QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QCOMPARE(control2->state(), QLowEnergyController::ConnectedState); QTRY_COMPARE(control2->error(), QLowEnergyController::NoError); control2->disconnectFromDevice(); QTRY_COMPARE(control2->state(), QLowEnergyController::UnconnectedState); QTRY_COMPARE(control->error(), QLowEnergyController::NoError); QTRY_COMPARE(control2->error(), QLowEnergyController::NoError); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); // reconnect control control->connectToDevice(); { QTRY_VERIFY_WITH_TIMEOUT(control->state() != QLowEnergyController::ConnectingState, 30000); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); } #endif } /* We are testing that we can run service discovery on the same device * for multiple services at the same time. * */ QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); // pick MAX_SERVICES_SAME_TIME_ACCESS services // and discover them at the same time #define MAX_SERVICES_SAME_TIME_ACCESS 3 QLowEnergyService *services[MAX_SERVICES_SAME_TIME_ACCESS]; QVERIFY(control->services().size() >= MAX_SERVICES_SAME_TIME_ACCESS); QList uuids = control->services(); // initialize services for (int i = 0; icreateServiceObject(uuids.at(i), this); QVERIFY(services[i]); } // start complete discovery for (int i = 0; idiscoverDetails(); // wait until discovery done for (int i = 0; iserviceUuid(); QTRY_VERIFY_WITH_TIMEOUT( services[i]->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); } // verify discovered services for (int i = 0; icontains(QLowEnergyCharacteristic())); QVERIFY(!services[i]->contains(QLowEnergyDescriptor())); } control->disconnectFromDevice(); QTRY_VERIFY_WITH_TIMEOUT(control->state() == QLowEnergyController::UnconnectedState, 30000); discoveryFinishedSpy.clear(); // redo the discovery with same controller QLowEnergyService *services_second[MAX_SERVICES_SAME_TIME_ACCESS]; control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); stateSpy.clear(); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); // get all details for (int i = 0; icreateServiceObject(uuids.at(i), this); QVERIFY(services_second[i]->parent() == this); QVERIFY(services[i]); QVERIFY(services_second[i]->state() == QLowEnergyService::RemoteService); services_second[i]->discoverDetails(); } // wait until discovery done for (int i = 0; iserviceUuid(); QTRY_VERIFY_WITH_TIMEOUT( services_second[i]->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); QCOMPARE(services_second[i]->serviceName(), services[i]->serviceName()); QCOMPARE(services_second[i]->serviceUuid(), services[i]->serviceUuid()); } // verify discovered services (1st and 2nd round) for (int i = 0; i chars = services[i]->characteristics(); for (qsizetype j = 0; j < chars.size(); ++j) { QCOMPARE(chars.at(j).isValid(), false); QVERIFY(services[i]->contains(chars[j])); QVERIFY(!services_second[i]->contains(chars[j])); const QList descriptors = chars[j].descriptors(); for (qsizetype k = 0; k < descriptors.size(); ++k) { QCOMPARE(descriptors[k].isValid(), false); services[i]->contains(descriptors[k]); QVERIFY(!services_second[i]->contains(chars[j])); } } QCOMPARE(services[i]->serviceUuid(), services_second[i]->serviceUuid()); QCOMPARE(services[i]->serviceName(), services_second[i]->serviceName()); QCOMPARE(services[i]->type(), services_second[i]->type()); QVERIFY(services[i]->state() == QLowEnergyService::InvalidService); QVERIFY(services_second[i]->state() == QLowEnergyService::RemoteServiceDiscovered); } // cleanup for (int i = 0; idisconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); } void tst_QLowEnergyController::verifyServiceProperties( const QLowEnergyService *info) { if (info->serviceUuid() == QBluetoothUuid(QString("00001800-0000-1000-8000-00805f9b34fb"))) { qDebug() << "Verifying GAP Service"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 5); // Device Name QString temp("00002a00-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[0].properties(), QLowEnergyCharacteristic::Read); QCOMPARE(chars[0].value(), QByteArray::fromHex("544920424c452053656e736f7220546167")); QVERIFY(chars[0].isValid()); QCOMPARE(chars[0].descriptors().size(), 0); QVERIFY(info->contains(chars[0])); // Appearance temp = QString("00002a01-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[1].properties(), QLowEnergyCharacteristic::Read); QCOMPARE(chars[1].value(), QByteArray::fromHex("0000")); QVERIFY(chars[1].isValid()); QCOMPARE(chars[1].descriptors().size(), 0); QVERIFY(info->contains(chars[1])); // Peripheral Privacy Flag temp = QString("00002a02-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); QVERIFY(chars[2].properties() & QLowEnergyCharacteristic::Read); QCOMPARE(chars[2].value(), QByteArray::fromHex("00")); QVERIFY(chars[2].isValid()); QCOMPARE(chars[2].descriptors().size(), 0); QVERIFY(info->contains(chars[2])); // Reconnection Address temp = QString("00002a03-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[3].uuid(), QBluetoothUuid(temp)); //Early firmware version had this characteristic as Read|Write and may fail QCOMPARE(chars[3].properties(), QLowEnergyCharacteristic::Write); if (chars[3].properties() & QLowEnergyCharacteristic::Read) QCOMPARE(chars[3].value(), QByteArray::fromHex("000000000000")); else QCOMPARE(chars[3].value(), QByteArray()); QVERIFY(chars[3].isValid()); QCOMPARE(chars[3].descriptors().size(), 0); QVERIFY(info->contains(chars[3])); // Peripheral Preferred Connection Parameters temp = QString("00002a04-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[4].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[4].properties(), QLowEnergyCharacteristic::Read); QCOMPARE(chars[4].value(), QByteArray::fromHex("5000a0000000e803")); QVERIFY(chars[4].isValid()); QCOMPARE(chars[4].descriptors().size(), 0); QVERIFY(info->contains(chars[4])); } else if (info->serviceUuid() == QBluetoothUuid(QString("00001801-0000-1000-8000-00805f9b34fb"))) { qDebug() << "Verifying GATT Service"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 1); // Service Changed QString temp("00002a05-0000-1000-8000-00805f9b34fb"); //this should really be readable according to GATT Service spec QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[0].properties(), QLowEnergyCharacteristic::Indicate); QCOMPARE(chars[0].value(), QByteArray()); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 1); QCOMPARE(chars[0].descriptors().at(0).isValid(), true); QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("0000180a-0000-1000-8000-00805f9b34fb"))) { qDebug() << "Verifying Device Information"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 9); // System ID QString temp("00002a23-0000-1000-8000-00805f9b34fb"); //this should really be readable according to GATT Service spec QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[0].properties(), QLowEnergyCharacteristic::Read); // Do not read the System ID as it is different for every device // QEXPECT_FAIL("", "The value is different on different devices", Continue); // QCOMPARE(chars[0].value(), QByteArray::fromHex("6e41ab0000296abc")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 0); // Model Number temp = QString("00002a24-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[1].properties(), QLowEnergyCharacteristic::Read); QCOMPARE(chars[1].value(), QByteArray::fromHex("4e2e412e00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 0); // Serial Number temp = QString("00002a25-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[2].value(), QByteArray::fromHex("4e2e412e00")); QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 0); // Firmware Revision temp = QString("00002a26-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[3].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[3].properties(), (QLowEnergyCharacteristic::Read)); //FW rev. : 1.5 (Oct 23 2013) // Other revisions will fail here QCOMPARE(chars[3].value(), QByteArray::fromHex("312e3520284f637420323320323031332900")); QVERIFY(chars[3].isValid()); QVERIFY(info->contains(chars[3])); QCOMPARE(chars[3].descriptors().size(), 0); // Hardware Revision temp = QString("00002a27-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[4].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[4].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[4].value(), QByteArray::fromHex("4e2e412e00")); QVERIFY(chars[4].isValid()); QVERIFY(info->contains(chars[4])); QCOMPARE(chars[4].descriptors().size(), 0); // Software Revision temp = QString("00002a28-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[5].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[5].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[5].value(), QByteArray::fromHex("4e2e412e00")); QVERIFY(chars[5].isValid()); QVERIFY(info->contains(chars[5])); QCOMPARE(chars[5].descriptors().size(), 0); // Manufacturer Name temp = QString("00002a29-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[6].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[6].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[6].value(), QByteArray::fromHex("546578617320496e737472756d656e747300")); QVERIFY(chars[6].isValid()); QVERIFY(info->contains(chars[6])); QCOMPARE(chars[6].descriptors().size(), 0); // IEEE temp = QString("00002a2a-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[7].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[7].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[7].value(), QByteArray::fromHex("fe006578706572696d656e74616c")); QVERIFY(chars[7].isValid()); QVERIFY(info->contains(chars[7])); QCOMPARE(chars[7].descriptors().size(), 0); // PnP ID temp = QString("00002a50-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[8].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[8].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[8].value(), QByteArray::fromHex("010d0000001001")); QVERIFY(chars[8].isValid()); QVERIFY(info->contains(chars[8])); QCOMPARE(chars[8].descriptors().size(), 0); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa00-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Temperature"; QList chars = info->characteristics(); QVERIFY(chars.size() >= 2); // Temp Data QString temp("f000aa01-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("00000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("54656d702e2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Temp Config temp = QString("f000aa02-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[1].descriptors().at(0).isValid(), true); QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("54656d702e20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); //Temp Period (introduced by later firmware versions) if (chars.size() > 2) { temp = QString("f000aa03-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[2].value(), QByteArray::fromHex("64")); QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[2].descriptors().at(0).isValid(), true); QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("54656d702e20506572696f64")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } } else if (info->serviceUuid() == QBluetoothUuid(QString("0000ffe0-0000-1000-8000-00805f9b34fb"))) { qDebug() << "Verifying Simple Keys"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 1); // Temp Data QString temp("0000ffe1-0000-1000-8000-00805f9b34fb"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray()); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("4b6579205072657373205374617465")); QVERIFY(info->contains(chars[0].descriptors().at(1))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa10-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Accelerometer"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 3); // Accel Data QString temp("f000aa11-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("416363656c2e2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Accel Config temp = QString("f000aa12-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("416363656c2e20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); // Accel Period temp = QString("f000aa13-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[2].value(), QByteArray::fromHex("64")); // don't change it or set it to 0x64 QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[2].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("416363656c2e20506572696f64")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa20-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Humidity"; QList chars = info->characteristics(); QVERIFY(chars.size() >= 2); //new firmware has more chars // Humidity Data QString temp("f000aa21-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("00000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("48756d69642e2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Humidity Config temp = QString("f000aa22-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("48756d69642e20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); if (chars.size() >= 3) { // New firmware new characteristic // Humidity Period temp = QString("f000aa23-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[2].value(), QByteArray::fromHex("64")); QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[2].descriptors().at(0).isValid(), true); QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("48756d69642e20506572696f64")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa30-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Magnetometer"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 3); // Magnetometer Data QString temp("f000aa31-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("000000000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("4d61676e2e2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Magnetometer Config temp = QString("f000aa32-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("4d61676e2e20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); // Magnetometer Period temp = QString("f000aa33-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[2].value(), QByteArray::fromHex("c8")); // don't change it or set it to 0xc8 QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); QCOMPARE(chars[2].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("4d61676e2e20506572696f64")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa40-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Pressure"; const QList chars = info->characteristics(); QVERIFY(chars.size() >= 3); // Pressure Data QString temp("f000aa41-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("00000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("4261726f6d2e2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Pressure Config temp = QString("f000aa42-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("4261726f6d2e20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); //calibration and period characteristic are swapped, ensure we don't depend on their order QLowEnergyCharacteristic calibration, period; for (const QLowEnergyCharacteristic &ch : chars) { //find calibration characteristic if (ch.uuid() == QBluetoothUuid(QString("f000aa43-0451-4000-b000-000000000000"))) calibration = ch; else if (ch.uuid() == QBluetoothUuid(QString("f000aa44-0451-4000-b000-000000000000"))) period = ch; } if (calibration.isValid()) { // Pressure Calibration temp = QString("f000aa43-0451-4000-b000-000000000000"); QCOMPARE(calibration.uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(calibration.properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(calibration.value(), QByteArray::fromHex("00000000000000000000000000000000")); // don't change it QVERIFY(calibration.isValid()); QVERIFY(info->contains(calibration)); QCOMPARE(calibration.descriptors().size(), 2); //descriptor checks QCOMPARE(calibration.descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(calibration.descriptors().at(0).uuid(), QBluetoothUuid( QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(calibration.descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(calibration.descriptors().at(0).value())); QVERIFY(info->contains(calibration.descriptors().at(0))); QCOMPARE(calibration.descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(calibration.descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(calibration.descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(calibration.descriptors().at(1).value(), QByteArray::fromHex("4261726f6d2e2043616c6962722e")); QVERIFY(info->contains(calibration.descriptors().at(1))); } if (period.isValid()) { // Period Calibration temp = QString("f000aa44-0451-4000-b000-000000000000"); QCOMPARE(period.uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(period.properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(period.value(), QByteArray::fromHex("64")); QVERIFY(period.isValid()); QVERIFY(info->contains(period)); QCOMPARE(period.descriptors().size(), 1); //descriptor checks QCOMPARE(period.descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(period.descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(period.descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(period.descriptors().at(0).value(), QByteArray::fromHex("4261726f6d2e20506572696f64")); QVERIFY(info->contains(period.descriptors().at(0))); } } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa50-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Gyroscope"; QList chars = info->characteristics(); QVERIFY(chars.size() >= 2); // Gyroscope Data QString temp("f000aa51-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); QCOMPARE(chars[0].value(), QByteArray::fromHex("000000000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("4779726f2044617461")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // Gyroscope Config temp = QString("f000aa52-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("4779726f20436f6e662e")); QVERIFY(info->contains(chars[1].descriptors().at(0))); // Gyroscope Period temp = QString("f000aa53-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[2].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[2].value(), QByteArray::fromHex("64")); QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[2].descriptors().at(0).isValid(), true); QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("4779726f20506572696f64")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000aa60-0451-4000-b000-000000000000"))) { qDebug() << "Verifying Test Service"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 2); // Test Data QString temp("f000aa61-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read)); QCOMPARE(chars[0].value(), QByteArray::fromHex("3f00")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 1); QCOMPARE(chars[0].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(0).value(), QByteArray::fromHex("546573742044617461")); QVERIFY(info->contains(chars[0].descriptors().at(0))); // Test Config temp = QString("f000aa62-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Write)); QCOMPARE(chars[1].value(), QByteArray::fromHex("00")); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); //descriptor checks QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("5465737420436f6e666967")); QVERIFY(info->contains(chars[1].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000ccc0-0451-4000-b000-000000000000"))) { qDebug() << "Connection Control Service"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 3); //first characteristic QString temp("f000ccc1-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Notify|QLowEnergyCharacteristic::Read)); // the connection control parameter change from platform to platform // better not test them here //QCOMPARE(chars[0].value(), QByteArray::fromHex("000000000000")); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("436f6e6e2e20506172616d73")); QVERIFY(info->contains(chars[0].descriptors().at(1))); //second characteristic temp = QString("f000ccc2-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[1].properties(), QLowEnergyCharacteristic::Write); QCOMPARE(chars[1].value(), QByteArray()); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 1); QCOMPARE(chars[1].descriptors().at(0).isValid(), true); QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(0).value(), QByteArray::fromHex("436f6e6e2e20506172616d7320526571")); QVERIFY(info->contains(chars[1].descriptors().at(0))); //third characteristic temp = QString("f000ccc3-0451-4000-b000-000000000000"); QCOMPARE(chars[2].uuid(), QBluetoothUuid(temp)); QCOMPARE(chars[2].properties(), QLowEnergyCharacteristic::Write); QCOMPARE(chars[2].value(), QByteArray()); QVERIFY(chars[2].isValid()); QVERIFY(info->contains(chars[2])); QCOMPARE(chars[2].descriptors().size(), 1); QCOMPARE(chars[2].descriptors().at(0).isValid(), true); QCOMPARE(chars[2].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[2].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[2].descriptors().at(0).value(), QByteArray::fromHex("446973636f6e6e65637420526571")); QVERIFY(info->contains(chars[2].descriptors().at(0))); } else if (info->serviceUuid() == QBluetoothUuid(QString("f000ffc0-0451-4000-b000-000000000000"))) { qDebug() << "Verifying OID Service"; QList chars = info->characteristics(); QCOMPARE(chars.size(), 2); // first characteristic QString temp("f000ffc1-0451-4000-b000-000000000000"); QCOMPARE(chars[0].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Notify|QLowEnergyCharacteristic::Write|QLowEnergyCharacteristic::WriteNoResponse)); QCOMPARE(chars[0].value(), QByteArray()); QVERIFY(chars[0].isValid()); QVERIFY(info->contains(chars[0])); QCOMPARE(chars[0].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[0].descriptors().at(0).isValid(), true); QCOMPARE(chars[0].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[0].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[0].descriptors().at(0))); QCOMPARE(chars[0].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[0].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[0].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[0].descriptors().at(1).value(), QByteArray::fromHex("496d67204964656e74696679")); QVERIFY(info->contains(chars[0].descriptors().at(1))); // second characteristic temp = QString("f000ffc2-0451-4000-b000-000000000000"); QCOMPARE(chars[1].uuid(), QBluetoothUuid(temp)); // value different in other revisions and test may fail QCOMPARE(chars[1].properties(), (QLowEnergyCharacteristic::Notify|QLowEnergyCharacteristic::Write|QLowEnergyCharacteristic::WriteNoResponse)); QCOMPARE(chars[1].value(), QByteArray()); QVERIFY(chars[1].isValid()); QVERIFY(info->contains(chars[1])); QCOMPARE(chars[1].descriptors().size(), 2); //descriptor checks QCOMPARE(chars[1].descriptors().at(0).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(0).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); QCOMPARE(chars[1].descriptors().at(0).type(), QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); QVERIFY(verifyClientCharacteristicValue(chars[0].descriptors().at(0).value())); QVERIFY(info->contains(chars[1].descriptors().at(0))); QCOMPARE(chars[1].descriptors().at(1).isValid(), true); // value different in other revisions and test may fail QCOMPARE(chars[1].descriptors().at(1).uuid(), QBluetoothUuid(QBluetoothUuid::DescriptorType::CharacteristicUserDescription)); QCOMPARE(chars[1].descriptors().at(1).type(), QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QCOMPARE(chars[1].descriptors().at(1).value(), QByteArray::fromHex("496d6720426c6f636b")); QVERIFY(info->contains(chars[1].descriptors().at(1))); } else { QFAIL(QString("Service not found" + info->serviceUuid().toString()).toUtf8().constData()); } } /* * CCC descriptors can have one of three distinct values: * 0000 - notifications and indications are off * 0100 - notifications enabled * 0200 - indications enabled * * The exact value is managed by the BTLE peripheral for each central * that connects. The value of this field is session based and may be retained * during multiple connections. * * This function returns \c true if the CCC value has a valid range. * */ bool tst_QLowEnergyController::verifyClientCharacteristicValue(const QByteArray &value) { if (value == QByteArray::fromHex("0000") || value == QByteArray::fromHex("0100") || value == QByteArray::fromHex("0200") ) return true; qWarning() << "Found incorrect CC value" << value.toHex(); return false; } void tst_QLowEnergyController::tst_defaultBehavior() { QList foundAddresses; const QList infos = QBluetoothLocalDevice::allDevices(); for (const QBluetoothHostInfo &info : infos) foundAddresses.append(info.address()); const QBluetoothAddress randomAddress("11:22:33:44:55:66"); // Test automatic detection of local adapter QScopedPointer controlDefaultAdapter(QLowEnergyController::createCentral( QBluetoothDeviceInfo(randomAddress, QString("random"), 1))); QCOMPARE(controlDefaultAdapter->remoteAddress(), randomAddress); QCOMPARE(controlDefaultAdapter->state(), QLowEnergyController::UnconnectedState); if (foundAddresses.isEmpty()) { QVERIFY(controlDefaultAdapter->localAddress().isNull()); } else { QCOMPARE(controlDefaultAdapter->error(), QLowEnergyController::NoError); QVERIFY(controlDefaultAdapter->errorString().isEmpty()); QVERIFY(foundAddresses.contains(controlDefaultAdapter->localAddress())); // unrelated uuids don't return valid service object // invalid service uuid QVERIFY(!controlDefaultAdapter->createServiceObject( QBluetoothUuid())); // some random uuid QVERIFY(!controlDefaultAdapter->createServiceObject( QBluetoothUuid(QBluetoothUuid::CharacteristicType::DeviceName))); } QCOMPARE(controlDefaultAdapter->services().size(), 0); // Test explicit local adapter if (!foundAddresses.isEmpty()) { QScopedPointer controlExplicitAdapter( QLowEnergyController::createCentral( QBluetoothDeviceInfo(randomAddress, QString("random"), 1), foundAddresses[0])); QCOMPARE(controlExplicitAdapter->remoteAddress(), randomAddress); QCOMPARE(controlExplicitAdapter->localAddress(), foundAddresses[0]); QCOMPARE(controlExplicitAdapter->state(), QLowEnergyController::UnconnectedState); QCOMPARE(controlExplicitAdapter->services().size(), 0); // unrelated uuids don't return valid service object // invalid service uuid QVERIFY(!controlExplicitAdapter->createServiceObject( QBluetoothUuid())); // some random uuid QVERIFY(!controlExplicitAdapter->createServiceObject( QBluetoothUuid(QBluetoothUuid::CharacteristicType::DeviceName))); } } void tst_QLowEnergyController::tst_writeCharacteristic() { #if !defined(Q_OS_MACOS) && !QT_CONFIG(winrt_bt) QList localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); #endif if (!remoteDeviceInfo.isValid()) QSKIP("No remote BTLE device found. Skipping test."); QScopedPointer control( QLowEnergyController::createCentral(remoteDeviceInfo)); QCOMPARE(control->error(), QLowEnergyController::NoError); control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QTRY_VERIFY_WITH_TIMEOUT(control->state() == QLowEnergyController::ConnectedState, 20000); QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); const QBluetoothUuid testService(QString("f000aa60-0451-4000-b000-000000000000")); QList uuids = control->services(); QVERIFY(uuids.contains(testService)); QLowEnergyService *service = control->createServiceObject(testService, this); QVERIFY(service); service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); // test service described by // http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User%27s_Guide const QList chars = service->characteristics(); QLowEnergyCharacteristic dataChar; QLowEnergyCharacteristic configChar; for (qsizetype i = 0; i < chars.size(); ++i) { if (chars[i].uuid() == QBluetoothUuid(QString("f000aa61-0451-4000-b000-000000000000"))) dataChar = chars[i]; else if (chars[i].uuid() == QBluetoothUuid(QString("f000aa62-0451-4000-b000-000000000000"))) configChar = chars[i]; } QVERIFY(dataChar.isValid()); QVERIFY(!(dataChar.properties() & ~QLowEnergyCharacteristic::Read)); // only a read char QVERIFY(service->contains(dataChar)); QVERIFY(configChar.isValid()); QVERIFY(configChar.properties() & QLowEnergyCharacteristic::Write); QVERIFY(configChar.properties() & QLowEnergyCharacteristic::Read); QVERIFY(service->contains(configChar)); QCOMPARE(dataChar.value(), QByteArray::fromHex("3f00")); QVERIFY(configChar.value() == QByteArray::fromHex("00") || configChar.value() == QByteArray::fromHex("81")); QSignalSpy writeSpy(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray))); QSignalSpy readSpy(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray))); // ******************************************* // test writing of characteristic // enable Blinking LED if not already enabled if (configChar.value() != QByteArray::fromHex("81")) { service->writeCharacteristic(configChar, QByteArray::fromHex("81")); //0x81 blink LED D1 QTRY_VERIFY_WITH_TIMEOUT(!writeSpy.isEmpty(), 10000); QCOMPARE(configChar.value(), QByteArray::fromHex("81")); QList firstSignalData = writeSpy.first(); QLowEnergyCharacteristic signalChar = firstSignalData[0].value(); QByteArray signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("81")); QVERIFY(signalChar == configChar); writeSpy.clear(); } // test direct read of configChar QVERIFY(readSpy.isEmpty()); service->readCharacteristic(configChar); QTRY_VERIFY_WITH_TIMEOUT(!readSpy.isEmpty(), 10000); QCOMPARE(configChar.value(), QByteArray::fromHex("81")); QCOMPARE(readSpy.size(), 1); //expect one characteristicRead signal { //verify the readCharacteristic() QList firstSignalData = readSpy.first(); QLowEnergyCharacteristic signalChar = firstSignalData[0].value(); QByteArray signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("81")); QCOMPARE(signalValue, configChar.value()); QVERIFY(signalChar == configChar); } service->writeCharacteristic(configChar, QByteArray::fromHex("00")); //turn LED D1 off QTRY_VERIFY_WITH_TIMEOUT(!writeSpy.isEmpty(), 10000); QCOMPARE(configChar.value(), QByteArray::fromHex("00")); QList firstSignalData = writeSpy.first(); QLowEnergyCharacteristic signalChar = firstSignalData[0].value(); QByteArray signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("00")); QVERIFY(signalChar == configChar); // ******************************************* // write wrong value -> error response required QSignalSpy errorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); writeSpy.clear(); QCOMPARE(errorSpy.size(), 0); QCOMPARE(writeSpy.size(), 0); // write 2 byte value to 1 byte characteristic service->writeCharacteristic(configChar, QByteArray::fromHex("1111")); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); QCOMPARE(errorSpy[0].at(0).value(), QLowEnergyService::CharacteristicWriteError); QCOMPARE(service->error(), QLowEnergyService::CharacteristicWriteError); QCOMPARE(writeSpy.size(), 0); QCOMPARE(configChar.value(), QByteArray::fromHex("00")); // ******************************************* // write to read-only characteristic -> error errorSpy.clear(); QCOMPARE(errorSpy.size(), 0); service->writeCharacteristic(dataChar, QByteArray::fromHex("ffff")); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); QCOMPARE(errorSpy[0].at(0).value(), QLowEnergyService::CharacteristicWriteError); QCOMPARE(service->error(), QLowEnergyService::CharacteristicWriteError); QCOMPARE(writeSpy.size(), 0); QCOMPARE(dataChar.value(), QByteArray::fromHex("3f00")); control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); // ******************************************* // write value while disconnected -> error errorSpy.clear(); QCOMPARE(errorSpy.size(), 0); service->writeCharacteristic(configChar, QByteArray::fromHex("ffff")); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 2000); QCOMPARE(errorSpy[0].at(0).value(), QLowEnergyService::OperationError); QCOMPARE(service->error(), QLowEnergyService::OperationError); QCOMPARE(writeSpy.size(), 0); QCOMPARE(configChar.value(), QByteArray::fromHex("00")); // invalid characteristics still belong to their respective service QVERIFY(service->contains(configChar)); QVERIFY(service->contains(dataChar)); QVERIFY(!service->contains(QLowEnergyCharacteristic())); delete service; } void tst_QLowEnergyController::tst_readWriteDescriptor() { #if !defined(Q_OS_MACOS) && !QT_CONFIG(winrt_bt) QList localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); #endif if (!remoteDeviceInfo.isValid()) QSKIP("No remote BTLE device found. Skipping test."); QScopedPointer control(QLowEnergyController::createCentral(remoteDeviceInfo)); // quick setup - more elaborate test is done by connect() control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); const QBluetoothUuid testService(QString("f000aa20-0451-4000-b000-000000000000")); QList uuids = control->services(); QVERIFY(uuids.contains(testService)); QLowEnergyService *service = control->createServiceObject(testService, this); QVERIFY(service); service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); // Humidity service described by // http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User%27s_Guide // 1. Find humidity data characteristic const QLowEnergyCharacteristic humidData = service->characteristic( QBluetoothUuid(QStringLiteral("f000aa21-0451-4000-b000-000000000000"))); const QLowEnergyCharacteristic humidConfig = service->characteristic( QBluetoothUuid(QStringLiteral("f000aa22-0451-4000-b000-000000000000"))); if (!humidData.isValid()) { delete service; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); QSKIP("Cannot find humidity data characteristic of TI Sensor"); } // 2. Find humidity data notification descriptor const QLowEnergyDescriptor notification = humidData.descriptor( QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); if (!notification.isValid()) { delete service; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); QSKIP("Cannot find humidity data notification of TI Sensor"); } QCOMPARE(notification.value(), QByteArray::fromHex("0000")); QVERIFY(service->contains(notification)); QVERIFY(service->contains(humidData)); if (humidConfig.isValid()) { QVERIFY(service->contains(humidConfig)); QCOMPARE(humidConfig.value(), QByteArray::fromHex("00")); } // 3. Test reading and writing to descriptor -> activate notifications QSignalSpy descWrittenSpy(service, SIGNAL(descriptorWritten(QLowEnergyDescriptor,QByteArray))); QSignalSpy descReadSpy(service, SIGNAL(descriptorRead(QLowEnergyDescriptor,QByteArray))); QSignalSpy charWrittenSpy(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray))); QSignalSpy charChangedSpy(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray))); QLowEnergyDescriptor signalDesc; QList firstSignalData; QByteArray signalValue; if (notification.value() != QByteArray::fromHex("0100")) { // enable notifications if not already done service->writeDescriptor(notification, QByteArray::fromHex("0100")); QTRY_VERIFY_WITH_TIMEOUT(!descWrittenSpy.isEmpty(), 3000); QCOMPARE(notification.value(), QByteArray::fromHex("0100")); firstSignalData = descWrittenSpy.first(); signalDesc = firstSignalData[0].value(); signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("0100")); QVERIFY(notification == signalDesc); descWrittenSpy.clear(); } // 4. Test reception of notifications // activate the humidity sensor if available if (humidConfig.isValid()) { service->writeCharacteristic(humidConfig, QByteArray::fromHex("01")); // first signal is confirmation of humidConfig write // subsequent signals are temp data updates QTRY_VERIFY_WITH_TIMEOUT(charWrittenSpy.size() == 1, 10000); QTRY_VERIFY_WITH_TIMEOUT(charChangedSpy.size() >= 4, 10000); QCOMPARE(charWrittenSpy.size(), 1); QLowEnergyCharacteristic writtenChar = charWrittenSpy[0].at(0).value(); QByteArray writtenValue = charWrittenSpy[0].at(1).toByteArray(); QCOMPARE(humidConfig, writtenChar); QCOMPARE(humidConfig.value(), writtenValue); QCOMPARE(writtenChar.value(), writtenValue); QCOMPARE(writtenValue, QByteArray::fromHex("01")); QList entry; for (qsizetype i = 0; i < charChangedSpy.size(); ++i) { entry = charChangedSpy[i]; const QLowEnergyCharacteristic ch = entry[0].value(); QCOMPARE(humidData, ch); //check last characteristic changed value matches the characteristics current value if (i == (charChangedSpy.size() - 1)) { writtenValue = entry[1].toByteArray(); QCOMPARE(ch.value(), writtenValue); QCOMPARE(humidData.value(), writtenValue); } } service->writeCharacteristic(humidConfig, QByteArray::fromHex("00")); } // 5. Test reading and writing of/to descriptor -> deactivate notifications service->readDescriptor(notification); QTRY_VERIFY_WITH_TIMEOUT(!descReadSpy.isEmpty(), 3000); QCOMPARE(descReadSpy.size(), 1); firstSignalData = descReadSpy.first(); signalDesc = firstSignalData[0].value(); signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, notification.value()); QCOMPARE(notification.value(), QByteArray::fromHex("0100")); descReadSpy.clear(); service->writeDescriptor(notification, QByteArray::fromHex("0000")); // verify QTRY_VERIFY_WITH_TIMEOUT(!descWrittenSpy.isEmpty(), 3000); QCOMPARE(notification.value(), QByteArray::fromHex("0000")); firstSignalData = descWrittenSpy.first(); signalDesc = firstSignalData[0].value(); signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("0000")); QVERIFY(notification == signalDesc); descWrittenSpy.clear(); // The series of wait calls below is required because toggling CCC via the notifying // property consistently crashes BlueZ 5.47. BlueZ 5.48 does not crash but // an error is thrown. For details see QTBUG-65729 if (isBluezDbusLE) QTest::qWait(1000); // test concurrent writeRequests // they need to be queued up service->writeDescriptor(notification,QByteArray::fromHex("0100")); if (isBluezDbusLE) QTest::qWait(1000); service->writeDescriptor(notification, QByteArray::fromHex("0000")); if (isBluezDbusLE) QTest::qWait(1000); service->writeDescriptor(notification, QByteArray::fromHex("0100")); if (isBluezDbusLE) QTest::qWait(1000); service->writeDescriptor(notification, QByteArray::fromHex("0000")); if (isBluezDbusLE) QTest::qWait(1000); QTRY_VERIFY_WITH_TIMEOUT(descWrittenSpy.size() == 4, 10000); QCOMPARE(notification.value(), QByteArray::fromHex("0000")); for (qsizetype i = 0; i < descWrittenSpy.size(); ++i) { firstSignalData = descWrittenSpy.at(i); signalDesc = firstSignalData[0].value(); signalValue = firstSignalData[1].toByteArray(); if (i & 0x1) // odd QCOMPARE(signalValue, QByteArray::fromHex("0000")); else // even QCOMPARE(signalValue, QByteArray::fromHex("0100")); QVERIFY(notification == signalDesc); } // 5. Test reading and writing of/to descriptor -> deactivate notifications service->readDescriptor(notification); QTRY_VERIFY_WITH_TIMEOUT(!descReadSpy.isEmpty(), 3000); QCOMPARE(descReadSpy.size(), 1); firstSignalData = descReadSpy.first(); signalDesc = firstSignalData[0].value(); signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, notification.value()); QCOMPARE(notification.value(), QByteArray::fromHex("0000")); descReadSpy.clear(); descWrittenSpy.clear(); // ******************************************* // write wrong value -> error response required QSignalSpy errorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); descWrittenSpy.clear(); QCOMPARE(errorSpy.size(), 0); QCOMPARE(descWrittenSpy.size(), 0); // write 4 byte value to 2 byte characteristic service->writeDescriptor(notification, QByteArray::fromHex("11112222")); #ifdef Q_OS_MAC // On OS X/iOS we have a special method to set notify value, // it accepts only false/true and not // writing descriptors, there is only one way to find this error - // immediately intercept in LE controller and set the error. QVERIFY(!errorSpy.isEmpty()); #else QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 30000); #endif QCOMPARE(errorSpy[0].at(0).value(), QLowEnergyService::DescriptorWriteError); QCOMPARE(service->error(), QLowEnergyService::DescriptorWriteError); QCOMPARE(descWrittenSpy.size(), 0); QCOMPARE(notification.value(), QByteArray::fromHex("0000")); control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); // ******************************************* // write value while disconnected -> error errorSpy.clear(); service->writeDescriptor(notification, QByteArray::fromHex("0100")); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 2000); QCOMPARE(errorSpy[0].at(0).value(), QLowEnergyService::OperationError); QCOMPARE(service->error(), QLowEnergyService::OperationError); QCOMPARE(descWrittenSpy.size(), 0); QCOMPARE(notification.value(), QByteArray::fromHex("0000")); delete service; } /* * By default this test is skipped. * * Following tests are performed: * - encrypted read and discovery * - readCharacteristic() of values longer than MTU * - readCharacteristic() if values equal to MTU * * This test is semi manual as the test device environment is very specific. * A programmable BTLE device is required. Currently, the test requires * the CSR Dev Kit using the hr_sensor example. * * The following changes must be done to example to be able to fully * utilise the test: * 1.) gap_service_db.db -> UUID_DEVICE_NAME char - add FLAG_ENCR_R * => tests encrypted read/discovery * 2.) dev_info_service_db.db -> UUID_DEVICE_INFO_MANUFACTURER_NAME * => The default name "Cambridge Silicon Radio" must be changed * to "Cambridge Silicon Radi" (new length 22) * 3.) revert change 1 above and redo test. This attempts to write a * char that is readable w/o encryption but writeable with encryption * => tests encryption code lines in writeCharacteristic() * => otherwise the read encryption would have increased security level already * => programmable CSR device must be reset before each run of this test * (to undo the previous write) */ void tst_QLowEnergyController::tst_customProgrammableDevice() { QSKIP("Skipping encryption"); //Adjust the uuids and device address as see fit to match //values that match the current test environment //The target characteristic must be readble and writable //under encryption to test dynamic switching of security level QBluetoothAddress encryptedDevice(QString("00:02:5B:00:15:10")); QBluetoothUuid serviceUuid(QBluetoothUuid::ServiceClassUuid::GenericAccess); QBluetoothUuid characterristicUuid(QBluetoothUuid::CharacteristicType::DeviceName); QScopedPointer control( QLowEnergyController::createCentral(QBluetoothDeviceInfo(encryptedDevice, QString("DeviceFoo"), 1))); QCOMPARE(control->error(), QLowEnergyController::NoError); control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); QList uuids = control->services(); QVERIFY(uuids.contains(serviceUuid)); QLowEnergyService *service = control->createServiceObject(serviceUuid, this); QVERIFY(service); // 1.) discovery triggers read of device name char which is encrypted service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); QLowEnergyCharacteristic encryptedChar = service->characteristic( characterristicUuid); const QByteArray encryptedReference("CSR HR Sensor"); QVERIFY(encryptedChar.isValid()); QCOMPARE(encryptedChar.value(), encryptedReference); // 2.) read of encrypted characteristic // => the discovery of the encrypted char above will have switched to // encryption already. QSignalSpy encryptedReadSpy(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray))); QSignalSpy encryptedErrorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); service->readCharacteristic(encryptedChar); QTRY_VERIFY_WITH_TIMEOUT(!encryptedReadSpy.isEmpty(), 10000); QVERIFY(encryptedErrorSpy.isEmpty()); QCOMPARE(encryptedReadSpy.size(), 1); QList entry = encryptedReadSpy[0]; QVERIFY(entry[0].value() == encryptedChar); QCOMPARE(entry[1].toByteArray(), encryptedReference); QCOMPARE(encryptedChar.value(), encryptedReference); // 3.) write to encrypted characteristic QSignalSpy encryptedWriteSpy(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray))); encryptedReadSpy.clear(); encryptedErrorSpy.clear(); const QByteArray newValue("ZZZ HR Sensor"); service->writeCharacteristic(encryptedChar, newValue); QTRY_VERIFY_WITH_TIMEOUT(!encryptedWriteSpy.isEmpty(), 10000); QVERIFY(encryptedErrorSpy.isEmpty()); QVERIFY(encryptedReadSpy.isEmpty()); QCOMPARE(encryptedWriteSpy.size(), 1); entry = encryptedWriteSpy[0]; QVERIFY(entry[0].value() == encryptedChar); QCOMPARE(entry[1].toByteArray(), newValue); QCOMPARE(encryptedChar.value(), newValue); delete service; //change to Device Information service QVERIFY(uuids.contains(QBluetoothUuid::ServiceClassUuid::DeviceInformation)); service = control->createServiceObject(QBluetoothUuid::ServiceClassUuid::DeviceInformation); QVERIFY(service); service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); // 4.) read of software revision string which is longer than mtu // tests readCharacteristic() including blob reads QSignalSpy readSpy(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray))); QSignalSpy errorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); const QByteArray expectedSoftRev("Application version 2.3.0.0"); QLowEnergyCharacteristic softwareRevChar = service->characteristic(QBluetoothUuid::CharacteristicType::SoftwareRevisionString); QVERIFY(softwareRevChar.isValid()); QCOMPARE(softwareRevChar.value(), expectedSoftRev); service->readCharacteristic(softwareRevChar); QTRY_VERIFY_WITH_TIMEOUT(!readSpy.isEmpty(), 10000); QVERIFY(errorSpy.isEmpty()); QCOMPARE(readSpy.size(), 1); entry = readSpy[0]; QVERIFY(entry[0].value() == softwareRevChar); QCOMPARE(entry[1].toByteArray(), expectedSoftRev); QCOMPARE(softwareRevChar.value(), expectedSoftRev); // 5.) read of manufacturer string which is exactly as long as single // MTU size (assuming negotiated MTU is 23) // => blob read test without blob being required // => the read blob answer will have zero length readSpy.clear(); // This assumes the manufacturer string was mondified via CSR SDK // see function description above const QByteArray expectedManufacturer("Cambridge Silicon Radi"); QLowEnergyCharacteristic manufacturerChar = service->characteristic(QBluetoothUuid::CharacteristicType::ManufacturerNameString); QVERIFY(manufacturerChar.isValid()); QCOMPARE(manufacturerChar.value(), expectedManufacturer); service->readCharacteristic(manufacturerChar); QTRY_VERIFY_WITH_TIMEOUT(!readSpy.isEmpty(), 10000); QVERIFY(errorSpy.isEmpty()); QCOMPARE(readSpy.size(), 1); entry = readSpy[0]; QVERIFY(entry[0].value() == manufacturerChar); QCOMPARE(entry[1].toByteArray(), expectedManufacturer); QCOMPARE(manufacturerChar.value(), expectedManufacturer); delete service; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); } /* 1.) Test with undiscovered devices - read and write invalid char 2.) Test with discovered devices - read non-readable char - write non-writable char */ void tst_QLowEnergyController::tst_errorCases() { #if !defined(Q_OS_MACOS) && !QT_CONFIG(winrt_bt) QList localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); #endif if (!remoteDeviceInfo.isValid()) QSKIP("No remote BTLE device found. Skipping test."); QScopedPointer control(QLowEnergyController::createCentral(remoteDeviceInfo)); QCOMPARE(control->error(), QLowEnergyController::NoError); control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QCOMPARE(control->state(), QLowEnergyController::ConnectedState); QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); // Setup required uuids const QBluetoothUuid irTemperaturServiceUuid(QStringLiteral("f000aa00-0451-4000-b000-000000000000")); const QBluetoothUuid irCharUuid(QString("f000aa01-0451-4000-b000-000000000000")); const QBluetoothUuid oadServiceUuid(QStringLiteral("f000ffc0-0451-4000-b000-000000000000")); const QBluetoothUuid oadCharUuid(QString("f000ffc1-0451-4000-b000-000000000000")); QVERIFY(control->services().contains(irTemperaturServiceUuid)); QVERIFY(control->services().contains(oadServiceUuid)); // Create service objects and basic tests QLowEnergyService *irService = control->createServiceObject(irTemperaturServiceUuid); QVERIFY(irService); QCOMPARE(irService->state(), QLowEnergyService::RemoteService); QVERIFY(irService->characteristics().isEmpty()); QLowEnergyService *oadService = control->createServiceObject(oadServiceUuid); QVERIFY(oadService); QCOMPARE(oadService->state(), QLowEnergyService::RemoteService); QVERIFY(oadService->characteristics().isEmpty()); QLowEnergyCharacteristic invalidChar; QLowEnergyDescriptor invalidDesc; QVERIFY(!irService->contains(invalidChar)); QVERIFY(!irService->contains(invalidDesc)); QSignalSpy irErrorSpy(irService, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); QSignalSpy oadErrorSpy(oadService, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); QSignalSpy irReadSpy(irService, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray))); QSignalSpy irWrittenSpy(irService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray))); QSignalSpy irDescReadSpy(irService, SIGNAL(descriptorRead(QLowEnergyDescriptor,QByteArray))); QSignalSpy irDescWrittenSpy(irService, SIGNAL(descriptorWritten(QLowEnergyDescriptor,QByteArray))); QSignalSpy oadCharReadSpy(oadService, SIGNAL(descriptorRead(QLowEnergyDescriptor,QByteArray))); // ******************************************************** // Test read/write to discovered service // with invalid characteristic & descriptor // discover IR Service irService->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( irService->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); QVERIFY(!irService->contains(invalidChar)); QVERIFY(!irService->contains(invalidDesc)); irErrorSpy.clear(); // read invalid characteristic irService->readCharacteristic(invalidChar); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QCOMPARE(irErrorSpy.size(), 1); QVERIFY(irWrittenSpy.isEmpty()); QVERIFY(irReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); irErrorSpy.clear(); // read invalid descriptor irService->readDescriptor(invalidDesc); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QCOMPARE(irErrorSpy.size(), 1); QVERIFY(irDescWrittenSpy.isEmpty()); QVERIFY(irDescReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); irErrorSpy.clear(); // write invalid characteristic irService->writeCharacteristic(invalidChar, QByteArray("foo")); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QCOMPARE(irErrorSpy.size(), 1); QVERIFY(irWrittenSpy.isEmpty()); QVERIFY(irReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); irErrorSpy.clear(); // write invalid descriptor irService->readDescriptor(invalidDesc); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QCOMPARE(irErrorSpy.size(), 1); QVERIFY(irDescWrittenSpy.isEmpty()); QVERIFY(irDescReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); irErrorSpy.clear(); // ******************************************************** // Test read/write to undiscovered service // with invalid characteristic & descriptor // read invalid characteristic oadService->readCharacteristic(invalidChar); QTRY_VERIFY_WITH_TIMEOUT(!oadErrorSpy.isEmpty(), 5000); QCOMPARE(oadErrorSpy.size(), 1); QCOMPARE(oadErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); oadErrorSpy.clear(); // read invalid descriptor oadService->readDescriptor(invalidDesc); QTRY_VERIFY_WITH_TIMEOUT(!oadErrorSpy.isEmpty(), 5000); QCOMPARE(oadErrorSpy.size(), 1); QCOMPARE(oadErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); oadErrorSpy.clear(); // write invalid characteristic oadService->writeCharacteristic(invalidChar, QByteArray("foo")); QTRY_VERIFY_WITH_TIMEOUT(!oadErrorSpy.isEmpty(), 5000); QCOMPARE(oadErrorSpy.size(), 1); QCOMPARE(oadErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); oadErrorSpy.clear(); // write invalid descriptor oadService->readDescriptor(invalidDesc); QTRY_VERIFY_WITH_TIMEOUT(!oadErrorSpy.isEmpty(), 5000); QCOMPARE(oadErrorSpy.size(), 1); QCOMPARE(oadErrorSpy[0].at(0).value(), QLowEnergyService::OperationError); oadErrorSpy.clear(); // ******************************************************** // Write to non-writable char QLowEnergyCharacteristic nonWritableChar = irService->characteristic(irCharUuid); QVERIFY(nonWritableChar.isValid()); // not writeable in any form QVERIFY(!(nonWritableChar.properties() & (QLowEnergyCharacteristic::Write|QLowEnergyCharacteristic::WriteNoResponse |QLowEnergyCharacteristic::WriteSigned))); irService->writeCharacteristic(nonWritableChar, QByteArray("ABCD")); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QVERIFY(irWrittenSpy.isEmpty()); QVERIFY(irReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::CharacteristicWriteError); irErrorSpy.clear(); // ******************************************************** // Write to non-writable desc // CharacteristicUserDescription is not writable QLowEnergyDescriptor nonWritableDesc = nonWritableChar.descriptor( QBluetoothUuid::DescriptorType::CharacteristicUserDescription); QVERIFY(nonWritableDesc.isValid()); irService->writeDescriptor(nonWritableDesc, QByteArray("ABCD")); QTRY_VERIFY_WITH_TIMEOUT(!irErrorSpy.isEmpty(), 5000); QVERIFY(irWrittenSpy.isEmpty()); QVERIFY(irReadSpy.isEmpty()); QCOMPARE(irErrorSpy[0].at(0).value(), QLowEnergyService::DescriptorWriteError); irErrorSpy.clear(); // ******************************************************** // Read non-readable char // discover OAD Service oadService->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( oadService->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); oadErrorSpy.clear(); // Test reading QLowEnergyCharacteristic oadChar = oadService->characteristic(oadCharUuid); QVERIFY(oadChar.isValid()); oadService->readCharacteristic(oadChar); QTRY_VERIFY_WITH_TIMEOUT(!oadErrorSpy.isEmpty(), 5000); QCOMPARE(oadErrorSpy.size(), 1); QVERIFY(oadCharReadSpy.isEmpty()); QCOMPARE(oadErrorSpy[0].at(0).value(), QLowEnergyService::CharacteristicReadError); oadErrorSpy.clear(); delete irService; delete oadService; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); } /* Tests write without responses. We utilize the Over-The-Air image update service of the SensorTag. */ void tst_QLowEnergyController::tst_writeCharacteristicNoResponse() { #if !defined(Q_OS_MACOS) && !QT_CONFIG(winrt_bt) QList localAdapters = QBluetoothLocalDevice::allDevices(); if (localAdapters.isEmpty()) QSKIP("No local Bluetooth device found. Skipping test."); #endif if (!remoteDeviceInfo.isValid()) QSKIP("No remote BTLE device found. Skipping test."); QScopedPointer control(QLowEnergyController::createCentral(remoteDeviceInfo)); QCOMPARE(control->error(), QLowEnergyController::NoError); control->connectToDevice(); { QTRY_IMPL(control->state() != QLowEnergyController::ConnectingState, 30000) } if (control->state() == QLowEnergyController::ConnectingState || control->error() != QLowEnergyController::NoError) { // default BTLE backend forever hangs in ConnectingState QSKIP("Cannot connect to remote device"); } QTRY_VERIFY_WITH_TIMEOUT(control->state() == QLowEnergyController::ConnectedState, 20000); QSignalSpy discoveryFinishedSpy(control.data(), SIGNAL(discoveryFinished())); QSignalSpy stateSpy(control.data(), SIGNAL(stateChanged(QLowEnergyController::ControllerState))); control->discoverServices(); QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.size() == 1, 20000); QCOMPARE(stateSpy.size(), 2); QCOMPARE(stateSpy.at(0).at(0).value(), QLowEnergyController::DiscoveringState); QCOMPARE(stateSpy.at(1).at(0).value(), QLowEnergyController::DiscoveredState); // The Over-The-Air update service uuid const QBluetoothUuid testService(QString("f000ffc0-0451-4000-b000-000000000000")); QList uuids = control->services(); QVERIFY(uuids.contains(testService)); QLowEnergyService *service = control->createServiceObject(testService, this); QVERIFY(service); service->discoverDetails(); QTRY_VERIFY_WITH_TIMEOUT( service->state() == QLowEnergyService::RemoteServiceDiscovered, 30000); // 1. Get "Image Identity" and "Image Block" characteristic const QLowEnergyCharacteristic imageIdentityChar = service->characteristic( QBluetoothUuid(QString("f000ffc1-0451-4000-b000-000000000000"))); const QLowEnergyCharacteristic imageBlockChar = service->characteristic( QBluetoothUuid(QString("f000ffc2-0451-4000-b000-000000000000"))); QVERIFY(imageIdentityChar.isValid()); QVERIFY(imageIdentityChar.properties() & QLowEnergyCharacteristic::Write); QVERIFY(imageIdentityChar.properties() & QLowEnergyCharacteristic::WriteNoResponse); QVERIFY(!(imageIdentityChar.properties() & QLowEnergyCharacteristic::Read)); //not readable QVERIFY(imageBlockChar.isValid()); // 2. Get "Image Identity" notification descriptor const QLowEnergyDescriptor identityNotification = imageIdentityChar.descriptor( QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); const QLowEnergyDescriptor blockNotification = imageBlockChar.descriptor( QBluetoothUuid(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)); if (!identityNotification.isValid() || !blockNotification.isValid() || !imageIdentityChar.isValid()) { delete service; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); QSKIP("Cannot find OAD char/notification"); } // 3. Enable notifications QSignalSpy descWrittenSpy(service, SIGNAL(descriptorWritten(QLowEnergyDescriptor,QByteArray))); QSignalSpy charChangedSpy(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray))); QSignalSpy charWrittenSpy(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray))); QSignalSpy charReadSpy(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray))); QSignalSpy errorSpy(service, SIGNAL(errorOccurred(QLowEnergyService::ServiceError))); //enable notifications on both characteristics if (identityNotification.value() != QByteArray::fromHex("0100")) { service->writeDescriptor(identityNotification, QByteArray::fromHex("0100")); QTRY_VERIFY_WITH_TIMEOUT(!descWrittenSpy.isEmpty(), 3000); QCOMPARE(identityNotification.value(), QByteArray::fromHex("0100")); QList firstSignalData = descWrittenSpy.first(); QLowEnergyDescriptor signalDesc = firstSignalData[0].value(); QByteArray signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("0100")); QVERIFY(identityNotification == signalDesc); descWrittenSpy.clear(); } if (blockNotification.value() != QByteArray::fromHex("0100")) { service->writeDescriptor(blockNotification, QByteArray::fromHex("0100")); QTRY_VERIFY_WITH_TIMEOUT(!descWrittenSpy.isEmpty(), 3000); QCOMPARE(blockNotification.value(), QByteArray::fromHex("0100")); QList firstSignalData = descWrittenSpy.first(); QLowEnergyDescriptor signalDesc = firstSignalData[0].value(); QByteArray signalValue = firstSignalData[1].toByteArray(); QCOMPARE(signalValue, QByteArray::fromHex("0100")); QVERIFY(blockNotification == signalDesc); descWrittenSpy.clear(); } QList entry; // Test direct read of non-readable characteristic QVERIFY(errorSpy.isEmpty()); QVERIFY(charReadSpy.isEmpty()); service->readCharacteristic(imageIdentityChar); QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); QCOMPARE(errorSpy.size(), 1); // should throw CharacteristicReadError QVERIFY(charReadSpy.isEmpty()); entry = errorSpy[0]; QCOMPARE(entry[0].value(), QLowEnergyService::CharacteristicReadError); // 4. Trigger image identity announcement (using traditional write) bool foundOneImage = false; // Image A // Write triggers a notification and write confirmation service->writeCharacteristic(imageIdentityChar, QByteArray::fromHex("0")); QTest::qWait(1000); QTRY_COMPARE_WITH_TIMEOUT(charChangedSpy.size(), 1, 5000); QTRY_COMPARE_WITH_TIMEOUT(charWrittenSpy.size(), 1, 5000); // This is very SensorTag specific logic. // If the image block is empty the current firmware // does not even send a notification for imageIdentityChar // but for imageBlockChar entry = charChangedSpy[0]; QLowEnergyCharacteristic first = entry[0].value(); QByteArray val1 = entry[1].toByteArray(); if (val1.size() == 8) { QCOMPARE(imageIdentityChar, first); foundOneImage = true; } else { // we received a notification for imageBlockChar QCOMPARE(imageBlockChar, first); qWarning() << "Invalid image A ident info"; } entry = charWrittenSpy[0]; QLowEnergyCharacteristic second = entry[0].value(); QByteArray val2 = entry[1].toByteArray(); QCOMPARE(imageIdentityChar, second); QVERIFY(val2 == QByteArray::fromHex("0") || val2 == val1); // notifications on non-readable characteristics do not update cache QVERIFY(imageIdentityChar.value().isEmpty()); QVERIFY(imageBlockChar.value().isEmpty()); charChangedSpy.clear(); charWrittenSpy.clear(); // Image B service->writeCharacteristic(imageIdentityChar, QByteArray::fromHex("1")); QTest::qWait(1000); QTRY_COMPARE_WITH_TIMEOUT(charChangedSpy.size(), 1, 5000); QTRY_COMPARE_WITH_TIMEOUT(charWrittenSpy.size(), 1, 5000);; entry = charChangedSpy[0]; first = entry[0].value(); val1 = entry[1].toByteArray(); if (val1.size() == 8) { QCOMPARE(imageIdentityChar, first); foundOneImage = true; } else { // we received a notification for imageBlockChar without explicitly // enabling them. This is caused by the device's default settings. QCOMPARE(imageBlockChar, first); qWarning() << "Invalid image B ident info"; } entry = charWrittenSpy[0]; second = entry[0].value(); val2 = entry[1].toByteArray(); QCOMPARE(imageIdentityChar, second); // notifications on non-readable characteristics do not update cache QVERIFY(imageIdentityChar.value().isEmpty()); QVERIFY(imageBlockChar.value().isEmpty()); /* Bluez resends the last confirmed write value, other platforms * send the value received by the change notification value. */ qDebug() << "Image B(1):" << val1.toHex() << val2.toHex(); QVERIFY(val2 == QByteArray::fromHex("1") || val2 == val1); QVERIFY2(foundOneImage, "The SensorTag doesn't have a valid image? (1)"); // 5. Trigger image identity announcement (without response) charChangedSpy.clear(); charWrittenSpy.clear(); foundOneImage = false; // Image A service->writeCharacteristic(imageIdentityChar, QByteArray::fromHex("0"), QLowEnergyService::WriteWithoutResponse); // we only expect one signal (the notification but not the write confirmation) // Wait at least a second for a potential second signals QTest::qWait(1000); QTRY_COMPARE_WITH_TIMEOUT(charChangedSpy.size(), 1, 10000); QTRY_COMPARE_WITH_TIMEOUT(charWrittenSpy.size(), 0, 10000); entry = charChangedSpy[0]; first = entry[0].value(); val1 = entry[1].toByteArray(); #ifdef Q_OS_ANDROID QEXPECT_FAIL("", "Android sends write confirmation when using WriteWithoutResponse", Continue); #endif QVERIFY(charWrittenSpy.isEmpty()); if (val1.size() == 8) { QCOMPARE(first, imageIdentityChar); foundOneImage = true; } else { // we received a notification for imageBlockChar without explicitly // enabling them. This is caused by the device's default settings. QCOMPARE(imageBlockChar, first); qWarning() << "Image A not set?"; } // notifications on non-readable characteristics do not update cache QVERIFY(imageIdentityChar.value().isEmpty()); QVERIFY(imageBlockChar.value().isEmpty()); charChangedSpy.clear(); // Image B service->writeCharacteristic(imageIdentityChar, QByteArray::fromHex("1"), QLowEnergyService::WriteWithoutResponse); // we only expect one signal (the notification but not the write confirmation) // Wait at least a second for a potential second signals QTest::qWait(1000); QTRY_COMPARE_WITH_TIMEOUT(charWrittenSpy.size(), 0, 10000); QTRY_COMPARE_WITH_TIMEOUT(charChangedSpy.size(), 1, 10000); entry = charChangedSpy[0]; first = entry[0].value(); val1 = entry[1].toByteArray(); #ifdef Q_OS_ANDROID QEXPECT_FAIL("", "Android sends write confirmation when using WriteWithoutResponse", Continue); #endif QVERIFY(charWrittenSpy.isEmpty()); if (val1.size() == 8) { QCOMPARE(first, imageIdentityChar); foundOneImage = true; } else { // we received a notification for imageBlockChar without explicitly // enabling them. This is caused by the device's default settings. QCOMPARE(imageBlockChar, first); qWarning() << "Image B not set?"; } // notifications on non-readable characteristics do not update cache QVERIFY(imageIdentityChar.value().isEmpty()); QVERIFY(imageBlockChar.value().isEmpty()); QVERIFY2(foundOneImage, "The SensorTag doesn't have a valid image? (2)"); delete service; control->disconnectFromDevice(); QTRY_COMPARE(control->state(), QLowEnergyController::UnconnectedState); QCOMPARE(control->error(), QLowEnergyController::NoError); } using namespace Qt::Literals::StringLiterals; void tst_QLowEnergyController::tst_rssiError() { // Create unconnected/invalid controller instances and verify that // reading RSSI value triggers error signal. For the actual // RSSI read testing see tst_qlowenergycontroller_device // Peripheral std::unique_ptr peripheral{QLowEnergyController::createPeripheral()}; QSignalSpy peripheralErrorSpy(peripheral.get(), &QLowEnergyController::errorOccurred); peripheral->readRssi(); QTRY_VERIFY(!peripheralErrorSpy.isEmpty()); QCOMPARE(peripheralErrorSpy.takeFirst().at(0).value(), QLowEnergyController::Error::RssiReadError); QCOMPARE(peripheral->error(), QLowEnergyController::Error::RssiReadError); // Central QBluetoothDeviceInfo info(QBluetoothAddress{u"11:22:33:44:55:66"_s}, u"invalid"_s, 1); std::unique_ptr central{QLowEnergyController::createCentral(info)}; QSignalSpy centralErrorSpy(central.get(), &QLowEnergyController::errorOccurred); central->readRssi(); QTRY_VERIFY(!centralErrorSpy.isEmpty()); QCOMPARE(centralErrorSpy.takeFirst().at(0).value(), QLowEnergyController::Error::RssiReadError); QCOMPARE(central->error(), QLowEnergyController::Error::RssiReadError); } QTEST_MAIN(tst_QLowEnergyController) #include "tst_qlowenergycontroller.moc"