diff options
-rw-r--r-- | src/bluetooth/qlowenergycharacteristic.cpp | 19 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycharacteristic.h | 4 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontrollernew_bluez.cpp | 65 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontrollernew_p.cpp | 8 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontrollernew_p.h | 6 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyservice.cpp | 54 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyservice.h | 7 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyserviceprivate_p.h | 2 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp | 150 |
9 files changed, 296 insertions, 19 deletions
diff --git a/src/bluetooth/qlowenergycharacteristic.cpp b/src/bluetooth/qlowenergycharacteristic.cpp index aa1795c3..d1c1a14c 100644 --- a/src/bluetooth/qlowenergycharacteristic.cpp +++ b/src/bluetooth/qlowenergycharacteristic.cpp @@ -162,6 +162,8 @@ QLowEnergyCharacteristic::PropertyTypes QLowEnergyCharacteristic::properties() c /*! Returns value of the gatt characteristic. + + The returned QByteArray contains the hex representation of the value. */ QByteArray QLowEnergyCharacteristic::value() const { @@ -186,17 +188,6 @@ QLowEnergyHandle QLowEnergyCharacteristic::handle() const } /*! - Sets the value \a value of the characteristic. This only caches the value. To write - a value directly to the device QLowEnergyController class must be used. - - \sa QLowEnergyController::writeCharacteristic() -*/ -void QLowEnergyCharacteristic::setValue(const QByteArray &value) -{ - //d_ptr->value = value; -} - -/*! Makes a copy of \a other and assigns it to this QLowEnergyCharacteristic object. The two copies continue to share the same service and registration details. */ @@ -276,7 +267,13 @@ bool QLowEnergyCharacteristic::isValid() const return true; } +QLowEnergyHandle QLowEnergyCharacteristic::attributeHandle() const +{ + if (d_ptr.isNull() || !data) + return 0; + return data->handle; +} /*! Returns the list of characteristic descriptors. diff --git a/src/bluetooth/qlowenergycharacteristic.h b/src/bluetooth/qlowenergycharacteristic.h index 9a14d57c..65f59c7a 100644 --- a/src/bluetooth/qlowenergycharacteristic.h +++ b/src/bluetooth/qlowenergycharacteristic.h @@ -82,7 +82,6 @@ public: QBluetoothUuid uuid() const; - void setValue(const QByteArray &value); //TODO shift to QLowEnergyControllerNew QByteArray value() const; QLowEnergyCharacteristic::PropertyTypes properties() const; @@ -93,9 +92,12 @@ public: bool isValid() const; protected: + QLowEnergyHandle attributeHandle() const; + QSharedPointer<QLowEnergyServicePrivate> d_ptr; friend class QLowEnergyService; + friend class QLowEnergyControllerNewPrivate; QLowEnergyCharacteristicPrivate *data; QLowEnergyCharacteristic(QSharedPointer<QLowEnergyServicePrivate> p, QLowEnergyHandle handle); diff --git a/src/bluetooth/qlowenergycontrollernew_bluez.cpp b/src/bluetooth/qlowenergycontrollernew_bluez.cpp index 400d2dbc..8aca2b54 100644 --- a/src/bluetooth/qlowenergycontrollernew_bluez.cpp +++ b/src/bluetooth/qlowenergycontrollernew_bluez.cpp @@ -64,12 +64,15 @@ #define ATT_OP_READ_RESPONSE 0xB #define ATT_OP_READ_BY_GROUP_REQUEST 0x10 //discover services #define ATT_OP_READ_BY_GROUP_RESPONSE 0x11 +#define ATT_OP_WRITE_REQUEST 0x12 //write characteristic +#define ATT_OP_WRITE_RESPONSE 0x13 //GATT command sizes in bytes #define FIND_INFO_REQUEST_SIZE 5 #define GRP_TYPE_REQ_SIZE 7 #define READ_BY_TYPE_REQ_SIZE 7 #define READ_REQUEST_SIZE 3 +#define WRITE_REQUEST_SIZE 3 // GATT error codes #define ATT_ERROR_INVALID_HANDLE 0x01 @@ -547,6 +550,28 @@ void QLowEnergyControllerNewPrivate::processReply( } } break; + case ATT_OP_WRITE_REQUEST: //error case + case ATT_OP_WRITE_RESPONSE: + { + //Write command response + Q_ASSERT(request.command == ATT_OP_WRITE_REQUEST); + + QLowEnergyHandle charHandle = request.reference.toUInt(); + QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); + if (service.isNull() || !service->characteristicList.contains(charHandle)) + break; + + if (isErrorResponse) { + service->setError(QLowEnergyService::CharacteristicWriteError); + break; + } + + const QByteArray newValue = request.reference2.toByteArray(); + service->characteristicList[charHandle].value = newValue; + QLowEnergyCharacteristic ch(service, charHandle); + emit service->characteristicChanged(ch, newValue); + } + break; default: qCDebug(QT_BT_BLUEZ) << "Unknown packet: " << response.toHex(); break; @@ -762,7 +787,7 @@ void QLowEnergyControllerNewPrivate::discoverNextDescriptor( bt_put_unaligned(htobs(charEndHandle), (quint16 *) &packet[3]); QByteArray data(FIND_INFO_REQUEST_SIZE, Qt::Uninitialized); - memcpy(data.data(), packet, FIND_INFO_REQUEST_SIZE); + memcpy(data.data(), packet, FIND_INFO_REQUEST_SIZE); Request request; request.payload = data; @@ -773,4 +798,42 @@ void QLowEnergyControllerNewPrivate::discoverNextDescriptor( sendNextPendingRequest(); } + +void QLowEnergyControllerNewPrivate::writeCharacteristic( + const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue) +{ + Q_ASSERT(!service.isNull()); + + if (!service->characteristicList.contains(charHandle)) + return; + + const QLowEnergyHandle valueHandle = service->characteristicList[charHandle].valueHandle; + const QByteArray rawData = QByteArray::fromHex(newValue); + // sizeof(command) + sizeof(handle) + sizeof(newValue) + const int size = 1 + 2 + rawData.size(); + + quint8 packet[WRITE_REQUEST_SIZE]; + packet[0] = ATT_OP_WRITE_REQUEST; + bt_put_unaligned(htobs(valueHandle), (quint16 *) &packet[1]); + + + QByteArray data(size, Qt::Uninitialized); + memcpy(data.data(), packet, WRITE_REQUEST_SIZE); + memcpy(&(data.data()[WRITE_REQUEST_SIZE]), rawData.constData(), rawData.size()); + + qCDebug(QT_BT_BLUEZ) << "Writing characteristic" << hex << charHandle + << "(size:" << size << ")"; + + Request request; + request.payload = data; + request.command = ATT_OP_WRITE_REQUEST; + request.reference = charHandle; + request.reference2 = newValue; + openRequests.enqueue(request); + + sendNextPendingRequest(); +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontrollernew_p.cpp b/src/bluetooth/qlowenergycontrollernew_p.cpp index 01a36faf..f2841d46 100644 --- a/src/bluetooth/qlowenergycontrollernew_p.cpp +++ b/src/bluetooth/qlowenergycontrollernew_p.cpp @@ -74,4 +74,12 @@ void QLowEnergyControllerNewPrivate::discoverServiceDetails(const QBluetoothUuid } +void QLowEnergyControllerNewPrivate::writeCharacteristic( + const QLowEnergyCharacteristic & /*characteristic*/, + const QLowEnergyHandle /*charHandle*/, + const QByteArray &/*newValue*/) +{ + +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontrollernew_p.h b/src/bluetooth/qlowenergycontrollernew_p.h index bac00e93..1d3ec095 100644 --- a/src/bluetooth/qlowenergycontrollernew_p.h +++ b/src/bluetooth/qlowenergycontrollernew_p.h @@ -77,7 +77,7 @@ public: void discoverServiceDetails(const QBluetoothUuid &service); - // misc lookup helpers + // misc helpers QSharedPointer<QLowEnergyServicePrivate> serviceForHandle( QLowEnergyHandle handle); void updateValueOfCharacteristic(QLowEnergyHandle charHandle, @@ -85,6 +85,10 @@ public: void updateValueOfDescriptor(QLowEnergyHandle charHandle, QLowEnergyHandle descriptorHandle, const QByteArray &value); + void writeCharacteristic(const QSharedPointer<QLowEnergyServicePrivate> service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue); + QBluetoothAddress remoteDevice; QBluetoothAddress localAdapter; diff --git a/src/bluetooth/qlowenergyservice.cpp b/src/bluetooth/qlowenergyservice.cpp index d8f80397..6469474f 100644 --- a/src/bluetooth/qlowenergyservice.cpp +++ b/src/bluetooth/qlowenergyservice.cpp @@ -70,6 +70,8 @@ QLowEnergyService::QLowEnergyService(QSharedPointer<QLowEnergyServicePrivate> p, this, SIGNAL(error(QLowEnergyService::ServiceError))); connect(p.data(), SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SIGNAL(stateChanged(QLowEnergyService::ServiceState))); + connect(p.data(), SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)), + this, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray))); } @@ -153,5 +155,57 @@ QLowEnergyService::ServiceError QLowEnergyService::error() const return d_ptr->lastError; } +bool QLowEnergyService::contains(const QLowEnergyCharacteristic &characteristic) +{ + if (characteristic.d_ptr.isNull() || !characteristic.data) + return false; + + if (d_ptr == characteristic.d_ptr + && d_ptr->characteristicList.contains(characteristic.attributeHandle())) { + return true; + } + + return false; +} + +/*! + Writes \a newValue as value for the \a characteristic. If the operation is successful + the \l characteristicChanged() signal will be emitted. \a newValue must contain the + hexadecimal representation of new value. + + A characteristic can only be written if this service is in the \l ServiceDiscovered state + and \a characteristic is writable. + */ +void QLowEnergyService::writeCharacteristic( + const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) +{ + //TODO check behavior when writing to WriteNoResponse characteristic + //TODO check behavior when writing to WriteSigned characteristic + //TODO add support for write long characteristic value (newValue.size() > MTU - 3) + Q_D(QLowEnergyService); + + // not a characteristic of this service + if (!contains(characteristic)) + return; + + // don't write if we don't have to + if (characteristic.value() == newValue) + return; + + // don't write write-protected or undiscovered characteristic + if (!(characteristic.properties() & QLowEnergyCharacteristic::Write) + || state() != ServiceDiscovered) { + d->setError(QLowEnergyService::OperationError); + return; + } + + if (!d->controller) + return; + + d->controller->writeCharacteristic(characteristic.d_ptr, + characteristic.attributeHandle(), + newValue); +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergyservice.h b/src/bluetooth/qlowenergyservice.h index 9c53aea7..d26d3fa8 100644 --- a/src/bluetooth/qlowenergyservice.h +++ b/src/bluetooth/qlowenergyservice.h @@ -60,7 +60,9 @@ public: enum ServiceError { NoError = 0, - ServiceNotValidError + ServiceNotValidError, + OperationError, + CharacteristicWriteError // emitted when writeCharacteristic() failed }; enum ServiceState { @@ -85,6 +87,9 @@ public: ServiceError error() const; + bool contains(const QLowEnergyCharacteristic &characteristic); + void writeCharacteristic(const QLowEnergyCharacteristic &characteristic, + const QByteArray &newValue); Q_SIGNALS: void stateChanged(QLowEnergyService::ServiceState newState); diff --git a/src/bluetooth/qlowenergyserviceprivate_p.h b/src/bluetooth/qlowenergyserviceprivate_p.h index 72caeeac..f5474864 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -86,6 +86,8 @@ public: signals: void stateChanged(QLowEnergyService::ServiceState newState); void error(QLowEnergyService::ServiceError error); + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, + const QByteArray &newValue); public: QLowEnergyHandle startHandle; diff --git a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp index 2e88d97f..66f458f6 100644 --- a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp +++ b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp @@ -72,6 +72,7 @@ private slots: void tst_connectNew(); void tst_concurrentDiscovery(); void tst_defaultBehavior(); + void tst_writeCharacteristic(); private: void verifyServiceProperties(const QLowEnergyServiceInfo &info); @@ -82,12 +83,17 @@ private: QList<QLowEnergyServiceInfo> foundServices; }; +Q_DECLARE_METATYPE(QLowEnergyCharacteristic) +Q_DECLARE_METATYPE(QLowEnergyService::ServiceError) + tst_QLowEnergyController::tst_QLowEnergyController() { qRegisterMetaType<QLowEnergyServiceInfo>("QLowEnergyServiceInfo"); qRegisterMetaType<QLowEnergyController::Error>("QLowEnergyController::Error"); + //qRegisterMetaType<QLowEnergyService::ServiceError>(); qRegisterMetaType<QLowEnergyCharacteristicInfo>("QLowEnergyCharacteristicInfo"); + qRegisterMetaType<QLowEnergyCharacteristic>(); QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); const QString remote = qgetenv("BT_TEST_DEVICE"); @@ -932,7 +938,7 @@ void tst_QLowEnergyController::tst_concurrentDiscovery() // initialize services for (int i = 0; i<MAX_SERVICES_SAME_TIME_ACCESS; i++) { - services[i] = control.createServiceObject(uuids.at(i)); + services[i] = control.createServiceObject(uuids.at(i), this); QVERIFY(services[i]); } @@ -971,8 +977,10 @@ void tst_QLowEnergyController::tst_concurrentDiscovery() // get all details for (int i = 0; i<MAX_SERVICES_SAME_TIME_ACCESS; i++) { - services_second[i] = control.createServiceObject(uuids.at(i)); + services_second[i] = control.createServiceObject(uuids.at(i), this); + QVERIFY(services_second[i]->parent() == this); QVERIFY(services[i]); + QVERIFY(services_second[i]->state() == QLowEnergyService::DiscoveryRequired); services_second[i]->discoverDetails(); } @@ -986,7 +994,6 @@ void tst_QLowEnergyController::tst_concurrentDiscovery() // verify discovered services (1st and 2nd round) for (int i = 0; i<MAX_SERVICES_SAME_TIME_ACCESS; i++) { - verifyServiceProperties(services_second[i]); //after disconnect all related characteristics and descriptors are invalid const QList<QLowEnergyCharacteristic> chars = services[i]->characteristics(); @@ -1198,7 +1205,7 @@ void tst_QLowEnergyController::verifyServiceProperties( QCOMPARE(chars[0].handle(), 0x25u); QCOMPARE(chars[0].properties(), (QLowEnergyCharacteristic::Read|QLowEnergyCharacteristic::Notify)); - //QCOMPARE(chars[0].value(), QByteArray("30303030")); + QCOMPARE(chars[0].value(), QByteArray("00000000")); QVERIFY(chars[0].isValid()); QCOMPARE(chars[0].descriptors().count(), 2); @@ -1762,6 +1769,141 @@ void tst_QLowEnergyController::tst_defaultBehavior() } +void tst_QLowEnergyController::tst_writeCharacteristic() +{ + QList<QBluetoothHostInfo> localAdapters = QBluetoothLocalDevice::allDevices(); + if (localAdapters.isEmpty() || remoteDevice.isNull()) + QSKIP("No local Bluetooth or remote BTLE device found. Skipping test."); + + // quick setup - more elaborate test is done by connectNew() + QLowEnergyControllerNew control(remoteDevice); + QCOMPARE(control.state(), QLowEnergyControllerNew::UnconnectedState); + QCOMPARE(control.error(), QLowEnergyControllerNew::NoError); + + control.connectToDevice(); + { + QTRY_IMPL(control.state() != QLowEnergyControllerNew::ConnectingState, + 30000); + } + + if (control.state() == QLowEnergyControllerNew::ConnectingState + || control.error() != QLowEnergyControllerNew::NoError) { + // default BTLE backend forever hangs in ConnectingState + QSKIP("Cannot connect to remote device"); + } + + QCOMPARE(control.state(), QLowEnergyControllerNew::ConnectedState); + QSignalSpy discoveryFinishedSpy(&control, SIGNAL(discoveryFinished())); + control.discoverServices(); + QTRY_VERIFY_WITH_TIMEOUT(discoveryFinishedSpy.count() == 1, 10000); + + const QBluetoothUuid testService(QString("f000aa60-0451-4000-b000-000000000000")); + QList<QBluetoothUuid> uuids = control.services(); + QVERIFY(uuids.contains(testService)); + + QLowEnergyService *service = control.createServiceObject(testService, this); + QVERIFY(service); + service->discoverDetails(); + QTRY_VERIFY_WITH_TIMEOUT( + service->state() == QLowEnergyService::ServiceDiscovered, 30000); + + //test service described by http://processors.wiki.ti.com/index.php/SensorTag_User_Guide + const QList<QLowEnergyCharacteristic> chars = service->characteristics(); + + QLowEnergyCharacteristic dataChar; + QLowEnergyCharacteristic configChar; + for (int i = 0; i < chars.count(); 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(configChar.isValid()); + QVERIFY(configChar.properties() & QLowEnergyCharacteristic::Write); + + QCOMPARE(dataChar.value(), QByteArray("3f00")); + QVERIFY(configChar.value() == QByteArray("00") || configChar.value() == QByteArray("81")); + + QSignalSpy writeSpy(service, + SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray))); + + // ******************************************* + // test writing of characteristic + // enable Blinking LED if not already enabled + if (configChar.value() != QByteArray("81")) { + service->writeCharacteristic(configChar, QByteArray("81")); //0x81 blink LED D1 + QTRY_VERIFY_WITH_TIMEOUT(!writeSpy.isEmpty(), 10000); + QCOMPARE(configChar.value(), QByteArray("81")); + QList<QVariant> firstSignalData= writeSpy.first(); + QLowEnergyCharacteristic signalChar = firstSignalData[0].value<QLowEnergyCharacteristic>(); + QByteArray signalValue = firstSignalData[1].toByteArray(); + + QCOMPARE(signalValue, QByteArray("81")); + QVERIFY(signalChar == configChar); + + writeSpy.clear(); + } + + service->writeCharacteristic(configChar, QByteArray("00")); //0x81 blink LED D1 + QTRY_VERIFY_WITH_TIMEOUT(!writeSpy.isEmpty(), 10000); + QCOMPARE(configChar.value(), QByteArray("00")); + QList<QVariant> firstSignalData = writeSpy.first(); + QLowEnergyCharacteristic signalChar = firstSignalData[0].value<QLowEnergyCharacteristic>(); + QByteArray signalValue = firstSignalData[1].toByteArray(); + + QCOMPARE(signalValue, QByteArray("00")); + QVERIFY(signalChar == configChar); + + // ******************************************* + // write wrong value -> error response required + QSignalSpy errorSpy(service, SIGNAL(error(QLowEnergyService::ServiceError))); + writeSpy.clear(); + QCOMPARE(errorSpy.count(), 0); + QCOMPARE(writeSpy.count(), 0); + + // write 2 byte value to 1 byte characteristic + service->writeCharacteristic(configChar, QByteArray("1111")); + QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); + QCOMPARE(errorSpy[0].at(0).value<QLowEnergyService::ServiceError>(), + QLowEnergyService::CharacteristicWriteError); + QCOMPARE(service->error(), QLowEnergyService::CharacteristicWriteError); + QCOMPARE(writeSpy.count(), 0); + QCOMPARE(configChar.value(), QByteArray("00")); + + // ******************************************* + // write to read-only characteristic -> error + errorSpy.clear(); + QCOMPARE(errorSpy.count(), 0); + service->writeCharacteristic(dataChar, QByteArray("ffff")); + + QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 10000); + QCOMPARE(errorSpy[0].at(0).value<QLowEnergyService::ServiceError>(), + QLowEnergyService::OperationError); + QCOMPARE(service->error(), QLowEnergyService::OperationError); + QCOMPARE(writeSpy.count(), 0); + QCOMPARE(dataChar.value(), QByteArray("3f00")); + + + control.disconnectFromDevice(); + + // ******************************************* + // write value while disconnected -> error + errorSpy.clear(); + QCOMPARE(errorSpy.count(), 0); + service->writeCharacteristic(configChar, QByteArray("ffff")); + QTRY_VERIFY_WITH_TIMEOUT(!errorSpy.isEmpty(), 2000); + QCOMPARE(errorSpy[0].at(0).value<QLowEnergyService::ServiceError>(), + QLowEnergyService::OperationError); + QCOMPARE(service->error(), QLowEnergyService::OperationError); + QCOMPARE(writeSpy.count(), 0); + QCOMPARE(configChar.value(), QByteArray("00")); + + delete service; +} + QTEST_MAIN(tst_QLowEnergyController) #include "tst_qlowenergycontroller.moc" |