diff options
-rw-r--r-- | src/bluetooth/qlowenergycharacteristicdata.cpp | 43 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycharacteristicdata.h | 4 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller.cpp | 2 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_bluez.cpp | 507 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_p.h | 63 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyservice.cpp | 73 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyservice.h | 3 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyserviceprivate.cpp | 2 | ||||
-rw-r--r-- | src/bluetooth/qlowenergyserviceprivate_p.h | 4 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp | 72 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp | 115 |
11 files changed, 763 insertions, 125 deletions
diff --git a/src/bluetooth/qlowenergycharacteristicdata.cpp b/src/bluetooth/qlowenergycharacteristicdata.cpp index f11779a5..a200f1c0 100644 --- a/src/bluetooth/qlowenergycharacteristicdata.cpp +++ b/src/bluetooth/qlowenergycharacteristicdata.cpp @@ -39,13 +39,19 @@ #include <QtCore/qloggingcategory.h> #include <QtCore/qdebug.h> +#include <climits> + QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT) struct QLowEnergyCharacteristicDataPrivate : public QSharedData { - QLowEnergyCharacteristicDataPrivate() : properties(QLowEnergyCharacteristic::Unknown) {} + QLowEnergyCharacteristicDataPrivate() + : properties(QLowEnergyCharacteristic::Unknown) + , minimumValueLength(0) + , maximumValueLength(INT_MAX) + {} QBluetoothUuid uuid; QLowEnergyCharacteristic::PropertyTypes properties; @@ -53,6 +59,8 @@ struct QLowEnergyCharacteristicDataPrivate : public QSharedData QByteArray value; QBluetooth::AttAccessConstraints readConstraints; QBluetooth::AttAccessConstraints writeConstraints; + int minimumValueLength; + int maximumValueLength; }; /*! @@ -197,6 +205,35 @@ QBluetooth::AttAccessConstraints QLowEnergyCharacteristicData::writeConstraints( } /*! + Specifies \a minimum and \a maximum to be the smallest and largest length, respectively, + that the value of this characteristic can have. The unit is bytes. If \a minimum and + \a maximum are equal, the characteristic has a fixed-length value. + */ +void QLowEnergyCharacteristicData::setValueLength(int minimum, int maximum) +{ + d->minimumValueLength = minimum; + d->maximumValueLength = qMax(minimum, maximum); +} + +/*! + Returns the minimum length in bytes that the value of this characteristic can have. + The default is zero. + */ +int QLowEnergyCharacteristicData::minimumValueLength() const +{ + return d->minimumValueLength; +} + +/*! + Returns the maximum length in bytes that the value of this characteristic can have. + By default, there is no limit beyond the constraints of the data type. + */ +int QLowEnergyCharacteristicData::maximumValueLength() const +{ + return d->maximumValueLength; +} + +/*! Returns true if and only if this characteristic is valid, that is, it has a non-null UUID. */ bool QLowEnergyCharacteristicData::isValid() const @@ -221,7 +258,9 @@ bool operator==(const QLowEnergyCharacteristicData &cd1, const QLowEnergyCharact && cd1.descriptors() == cd2.descriptors() && cd1.value() == cd2.value() && cd1.readConstraints() == cd2.readConstraints() - && cd1.writeConstraints() == cd2.writeConstraints()); + && cd1.writeConstraints() == cd2.writeConstraints() + && cd1.minimumValueLength() == cd2.maximumValueLength() + && cd1.maximumValueLength() == cd2.maximumValueLength()); } /*! diff --git a/src/bluetooth/qlowenergycharacteristicdata.h b/src/bluetooth/qlowenergycharacteristicdata.h index dc99f002..56493198 100644 --- a/src/bluetooth/qlowenergycharacteristicdata.h +++ b/src/bluetooth/qlowenergycharacteristicdata.h @@ -70,6 +70,10 @@ public: void setWriteConstraints(QBluetooth::AttAccessConstraints constraints); QBluetooth::AttAccessConstraints writeConstraints() const; + void setValueLength(int minimum, int maximum); + int minimumValueLength() const; + int maximumValueLength() const; + bool isValid() const; void swap(QLowEnergyCharacteristicData &other) Q_DECL_NOTHROW { qSwap(d, other.d); } diff --git a/src/bluetooth/qlowenergycontroller.cpp b/src/bluetooth/qlowenergycontroller.cpp index cf166281..d4d41d69 100644 --- a/src/bluetooth/qlowenergycontroller.cpp +++ b/src/bluetooth/qlowenergycontroller.cpp @@ -838,6 +838,8 @@ QLowEnergyService *QLowEnergyController::addService(const QLowEnergyServiceData // for it. const auto servicePrivate = QSharedPointer<QLowEnergyServicePrivate>::create(); + servicePrivate->state = QLowEnergyService::LocalService; + servicePrivate->setController(d_ptr); servicePrivate->uuid = service.uuid(); servicePrivate->type = service.type() == QLowEnergyServiceData::ServiceTypePrimary ? QLowEnergyService::PrimaryService : QLowEnergyService::IncludedService; diff --git a/src/bluetooth/qlowenergycontroller_bluez.cpp b/src/bluetooth/qlowenergycontroller_bluez.cpp index a298ea25..2ebff8ab 100644 --- a/src/bluetooth/qlowenergycontroller_bluez.cpp +++ b/src/bluetooth/qlowenergycontroller_bluez.cpp @@ -39,6 +39,7 @@ #include "bluez/hcimanager_p.h" #include <QtCore/QLoggingCategory> +#include <QtBluetooth/QBluetoothLocalDevice> #include <QtBluetooth/QBluetoothSocket> #include <QtBluetooth/QLowEnergyCharacteristicData> #include <QtBluetooth/QLowEnergyDescriptorData> @@ -46,6 +47,7 @@ #include <QtBluetooth/QLowEnergyServiceData> #include <algorithm> +#include <climits> #include <cstring> #include <errno.h> #include <sys/types.h> @@ -239,7 +241,6 @@ template<> void putDataAndIncrement(const QByteArray &value, char *&dst) dst += value.count(); } - QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() : QObject(), state(QLowEnergyController::UnconnectedState), @@ -433,6 +434,8 @@ void QLowEnergyControllerPrivate::l2cpDisconnected() { Q_Q(QLowEnergyController); + if (role == QLowEnergyController::PeripheralRole) + storeClientConfigurations(); invalidateServices(); resetController(); setState(QLowEnergyController::UnconnectedState); @@ -472,6 +475,8 @@ void QLowEnergyControllerPrivate::resetController() { openRequests.clear(); openPrepareWriteRequests.clear(); + scheduledIndications.clear(); + indicationInFlight = false; requestPending = false; encryptionChangePending = false; receivedMtuExchangeRequest = false; @@ -539,6 +544,14 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() case ATT_OP_EXECUTE_WRITE_REQUEST: handleExecuteWriteRequest(incomingPacket); return; + case ATT_OP_HANDLE_VAL_CONFIRMATION: + if (indicationInFlight) { + indicationInFlight = false; + sendNextIndication(); + } else { + qCWarning(QT_BT_BLUEZ) << "received unexpected handle value confirmation"; + } + return; default: //only solicited replies finish pending requests requestPending = false; @@ -1714,49 +1727,13 @@ void QLowEnergyControllerPrivate::writeCharacteristic( if (!service->characteristicList.contains(charHandle)) return; - const QLowEnergyHandle valueHandle = service->characteristicList[charHandle].valueHandle; - const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); - - quint8 packet[WRITE_REQUEST_HEADER_SIZE]; - if (writeWithResponse) { - if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { - sendNextPrepareWriteRequest(charHandle, newValue, 0); - sendNextPendingRequest(); - return; - } else { - // write value fits into single package - packet[0] = ATT_OP_WRITE_REQUEST; - } + QLowEnergyServicePrivate::CharData &charData = service->characteristicList[charHandle]; + if (role == QLowEnergyController::PeripheralRole) { + writeCharacteristicForPeripheral(charData, newValue); } else { - // write without response - packet[0] = ATT_OP_WRITE_COMMAND; - } - - bt_put_unaligned(htobs(valueHandle), (quint16 *) &packet[1]); - - QByteArray data(size, Qt::Uninitialized); - memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); - memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); - - qCDebug(QT_BT_BLUEZ) << "Writing characteristic" << hex << charHandle - << "(size:" << size << "with response:" << writeWithResponse << ")"; - - // Advantage of write without response is the quick turnaround. - // It can be send at any time and does not produce responses. - // Therefore we will not put them into the openRequest queue at all. - if (!writeWithResponse) { - sendPacket(data); - return; + writeCharacteristicForCentral(charHandle, charData.valueHandle, newValue, + writeWithResponse); } - - Request request; - request.payload = data; - request.command = ATT_OP_WRITE_REQUEST; - request.reference = charHandle; - request.reference2 = newValue; - openRequests.enqueue(request); - - sendNextPendingRequest(); } void QLowEnergyControllerPrivate::writeDescriptor( @@ -1767,32 +1744,10 @@ void QLowEnergyControllerPrivate::writeDescriptor( { Q_ASSERT(!service.isNull()); - if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { - sendNextPrepareWriteRequest(descriptorHandle, newValue, 0); - sendNextPendingRequest(); - return; - } - - quint8 packet[WRITE_REQUEST_HEADER_SIZE]; - packet[0] = ATT_OP_WRITE_REQUEST; - bt_put_unaligned(htobs(descriptorHandle), (quint16 *) &packet[1]); - - const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); - QByteArray data(size, Qt::Uninitialized); - memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); - memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); - - qCDebug(QT_BT_BLUEZ) << "Writing descriptor" << hex << descriptorHandle - << "(size:" << size << ")"; - - Request request; - request.payload = data; - request.command = ATT_OP_WRITE_REQUEST; - request.reference = (charHandle | (descriptorHandle << 16)); - request.reference2 = newValue; - openRequests.enqueue(request); - - sendNextPendingRequest(); + if (role == QLowEnergyController::PeripheralRole) + writeDescriptorForPeripheral(service, charHandle, descriptorHandle, newValue); + else + writeDescriptorForCentral(charHandle, descriptorHandle, newValue); } /*! @@ -2251,14 +2206,205 @@ void QLowEnergyControllerPrivate::handleReadByGroupTypeRequest(const QByteArray sendListResponse(responsePrefix, elementSize, results, elemWriter); } +void QLowEnergyControllerPrivate::updateLocalAttributeValue( + QLowEnergyHandle handle, + const QByteArray &value, + QLowEnergyCharacteristic &characteristic, + QLowEnergyDescriptor &descriptor) +{ + localAttributes[handle].value = value; + foreach (const auto &service, localServices) { + if (handle < service->startHandle || handle > service->endHandle) + continue; + for (auto charIt = service->characteristicList.begin(); + charIt != service->characteristicList.end(); ++charIt) { + QLowEnergyServicePrivate::CharData &charData = charIt.value(); + if (handle == charIt.key() + 1) { // Char value decl comes right after char decl. + charData.value = value; + characteristic = QLowEnergyCharacteristic(service, charIt.key()); + return; + } + for (auto descIt = charData.descriptorList.begin(); + descIt != charData.descriptorList.end(); ++descIt) { + if (handle == descIt.key()) { + descIt.value().value = value; + descriptor = QLowEnergyDescriptor(service, charIt.key(), handle); + return; + } + } + } + } + qFatal("local services map inconsistent with local attribute map"); +} + +static bool isNotificationEnabled(quint16 clientConfigValue) { return clientConfigValue & 0x1; } +static bool isIndicationEnabled(quint16 clientConfigValue) { return clientConfigValue & 0x2; } + +void QLowEnergyControllerPrivate::writeCharacteristicForPeripheral( + QLowEnergyServicePrivate::CharData &charData, + const QByteArray &newValue) +{ + const QLowEnergyHandle valueHandle = charData.valueHandle; + Q_ASSERT(valueHandle <= lastLocalHandle); + Attribute &attribute = localAttributes[valueHandle]; + if (newValue.count() < attribute.minLength || newValue.count() > attribute.maxLength) { + qCWarning(QT_BT_BLUEZ) << "ignoring value of invalid length" << newValue.count() + << "for attribute" << valueHandle; + return; + } + attribute.value = newValue; + charData.value = newValue; + const bool hasNotifyProperty = attribute.properties & QLowEnergyCharacteristic::Notify; + const bool hasIndicateProperty + = attribute.properties & QLowEnergyCharacteristic::Indicate; + if (!hasNotifyProperty && !hasIndicateProperty) + return; + foreach (const QLowEnergyServicePrivate::DescData &desc, charData.descriptorList) { + if (desc.uuid != QBluetoothUuid::ClientCharacteristicConfiguration) + continue; + + // Notify/indicate currently connected client. + const bool isConnected = state == QLowEnergyController::ConnectedState; + if (isConnected) { + Q_ASSERT(desc.value.count() == 2); + quint16 configValue = bt_get_le16(desc.value.constData()); + if (isNotificationEnabled(configValue) && hasNotifyProperty) { + sendNotification(valueHandle); + } else if (isIndicationEnabled(configValue) && hasIndicateProperty) { + if (indicationInFlight) + scheduledIndications << valueHandle; + else + sendIndication(valueHandle); + } + } + + // Prepare notification/indication of unconnected, bonded clients. + for (auto it = clientConfigData.begin(); it != clientConfigData.end(); ++it) { + if (isConnected && it.key() == remoteDevice.toUInt64()) + continue; + QVector<ClientConfigurationData> &configDataList = it.value(); + for (ClientConfigurationData &configData : configDataList) { + if (configData.charValueHandle != valueHandle) + continue; + if ((isNotificationEnabled(configData.configValue) && hasNotifyProperty) + || (isIndicationEnabled(configData.configValue) && hasIndicateProperty)) { + configData.charValueWasUpdated = true; + break; + } + } + } + break; + } +} + +void QLowEnergyControllerPrivate::writeCharacteristicForCentral( + QLowEnergyHandle charHandle, + QLowEnergyHandle valueHandle, + const QByteArray &newValue, + bool writeWithResponse) +{ + const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); + + quint8 packet[WRITE_REQUEST_HEADER_SIZE]; + if (writeWithResponse) { + if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { + sendNextPrepareWriteRequest(charHandle, newValue, 0); + sendNextPendingRequest(); + return; + } else { + // write value fits into single package + packet[0] = ATT_OP_WRITE_REQUEST; + } + } else { + // write without response + packet[0] = ATT_OP_WRITE_COMMAND; + } + + bt_put_unaligned(htobs(valueHandle), (quint16 *) &packet[1]); + + QByteArray data(size, Qt::Uninitialized); + memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); + memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); + + qCDebug(QT_BT_BLUEZ) << "Writing characteristic" << hex << charHandle + << "(size:" << size << "with response:" << writeWithResponse << ")"; + + // Advantage of write without response is the quick turnaround. + // It can be send at any time and does not produce responses. + // Therefore we will not put them into the openRequest queue at all. + if (!writeWithResponse) { + sendPacket(data); + return; + } + + Request request; + request.payload = data; + request.command = ATT_OP_WRITE_REQUEST; + request.reference = charHandle; + request.reference2 = newValue; + openRequests.enqueue(request); + + sendNextPendingRequest(); +} + +void QLowEnergyControllerPrivate::writeDescriptorForPeripheral( + const QSharedPointer<QLowEnergyServicePrivate> &service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle, + const QByteArray &newValue) +{ + Q_ASSERT(descriptorHandle <= lastLocalHandle); + Attribute &attribute = localAttributes[descriptorHandle]; + if (newValue.count() < attribute.minLength || newValue.count() > attribute.maxLength) { + qCWarning(QT_BT_BLUEZ) << "invalid value of size" << newValue.count() + << "for attribute" << descriptorHandle; + return; + } + attribute.value = newValue; + service->characteristicList[charHandle].descriptorList[descriptorHandle].value = newValue; +} + +void QLowEnergyControllerPrivate::writeDescriptorForCentral( + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle, + const QByteArray &newValue) +{ + if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { + sendNextPrepareWriteRequest(descriptorHandle, newValue, 0); + sendNextPendingRequest(); + return; + } + + quint8 packet[WRITE_REQUEST_HEADER_SIZE]; + packet[0] = ATT_OP_WRITE_REQUEST; + bt_put_unaligned(htobs(descriptorHandle), (quint16 *) &packet[1]); + + const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); + QByteArray data(size, Qt::Uninitialized); + memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); + memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); + + qCDebug(QT_BT_BLUEZ) << "Writing descriptor" << hex << descriptorHandle + << "(size:" << size << ")"; + + Request request; + request.payload = data; + request.command = ATT_OP_WRITE_REQUEST; + request.reference = (charHandle | (descriptorHandle << 16)); + request.reference2 = newValue; + openRequests.enqueue(request); + + sendNextPendingRequest(); +} + void QLowEnergyControllerPrivate::handleWriteRequestOrCommand(const QByteArray &packet) { // Spec v4.2, Vol 3, Part F, 3.4.5.1-3 - if (!checkPacketSize(packet, 3, mtuSize)) - return; const bool isRequest = packet.at(0) == ATT_OP_WRITE_REQUEST; const bool isSigned = packet.at(0) == ATT_OP_SIGNED_WRITE_COMMAND; + if (!checkPacketSize(packet, isSigned ? 15 : 3, mtuSize)) + return; const QLowEnergyHandle handle = bt_get_le16(packet.constData() + 1); qCDebug(QT_BT_BLUEZ) << "client sends" << (isSigned ? "signed" : "") << "write" << (isRequest ? "request" : "command") << "for handle" << handle; @@ -2266,15 +2412,16 @@ void QLowEnergyControllerPrivate::handleWriteRequestOrCommand(const QByteArray & if (!checkHandle(packet, handle)) return; + int valueLength; if (isSigned) { // const QByteArray signature = packet.right(12); return; // TODO: Check signature and continue if it's valid. Store sign counter. - // TODO: extract value + valueLength = packet.count() - 15; } else { - // TODO: Extract value. + valueLength = packet.count() - 3; } - const Attribute &attribute = localAttributes.at(handle); + Attribute &attribute = localAttributes[handle]; const QLowEnergyCharacteristic::PropertyType type = isRequest ? QLowEnergyCharacteristic::Write : isSigned ? QLowEnergyCharacteristic::WriteSigned : QLowEnergyCharacteristic::WriteNoResponse; @@ -2283,16 +2430,32 @@ void QLowEnergyControllerPrivate::handleWriteRequestOrCommand(const QByteArray & sendErrorResponse(packet.at(0), handle, permissionsError); return; } - if (false /* value is greater than maximum */) { + if (valueLength > attribute.maxLength) { sendErrorResponse(packet.at(0), handle, ATT_ERROR_INVAL_ATTR_VALUE_LEN); return; } - // TODO: Write attribute + // If the attribute value has a fixed size and the value in the packet is shorter, + // then we overwrite only the start of the attribute value and keep the rest. + QByteArray value = packet.mid(3, valueLength); + if (attribute.minLength == attribute.maxLength && valueLength < attribute.minLength) + value += attribute.value.mid(valueLength, attribute.maxLength - valueLength); - if (isRequest) - sendErrorResponse(packet.at(0), 0, ATT_ERROR_REQUEST_NOT_SUPPORTED); - //sendPacket(QByteArray(1, ATT_OP_WRITE_RESPONSE)); + QLowEnergyCharacteristic characteristic; + QLowEnergyDescriptor descriptor; + updateLocalAttributeValue(handle, value, characteristic, descriptor); + + if (isRequest) { + const QByteArray response = QByteArray(1, ATT_OP_WRITE_RESPONSE); + sendPacket(response); + } + + if (characteristic.isValid()) { + emit characteristic.d_ptr->characteristicChanged(characteristic, value); + } else { + Q_ASSERT(descriptor.isValid()); + emit descriptor.d_ptr->descriptorWritten(descriptor, value); + } } void QLowEnergyControllerPrivate::handlePrepareWriteRequest(const QByteArray &packet) @@ -2338,22 +2501,40 @@ void QLowEnergyControllerPrivate::handleExecuteWriteRequest(const QByteArray &pa QVector<WriteRequest> requests = openPrepareWriteRequests; openPrepareWriteRequests.clear(); + QVector<QLowEnergyCharacteristic> characteristics; + QVector<QLowEnergyDescriptor> descriptors; if (!cancel) { - if (false /* maximum value size exceeded */) { - sendErrorResponse(packet.at(0), 0 /* handle? */, ATT_ERROR_INVAL_ATTR_VALUE_LEN); - return; - } - if (false /* offset invalid */) { - sendErrorResponse(packet.at(0), 0 /* handle? */, ATT_ERROR_INVALID_OFFSET); - return; + foreach (const WriteRequest &request, requests) { + Attribute &attribute = localAttributes[request.handle]; + if (request.valueOffset > attribute.value.count()) { + sendErrorResponse(packet.at(0), request.handle, ATT_ERROR_INVALID_OFFSET); + return; + } + const QByteArray newValue = attribute.value.left(request.valueOffset) + request.value; + if (newValue.count() > attribute.maxLength) { + sendErrorResponse(packet.at(0), request.handle, ATT_ERROR_INVAL_ATTR_VALUE_LEN); + return; + } + QLowEnergyCharacteristic characteristic; + QLowEnergyDescriptor descriptor; + // TODO: Redundant attribute lookup for the case of the same handle appearing + // more than once. + updateLocalAttributeValue(request.handle, newValue, characteristic, descriptor); + if (characteristic.isValid()) { + characteristics << characteristic; + } else if (descriptor.isValid()) { + Q_ASSERT(descriptor.isValid()); + descriptors << descriptor; + } } + } - // TODO: Write attribute. + sendPacket(QByteArray(1, ATT_OP_EXECUTE_WRITE_RESPONSE)); - } - openPrepareWriteRequests.clear(); - sendErrorResponse(packet.at(0), 0, ATT_ERROR_REQUEST_NOT_SUPPORTED); - //sendPacket(QByteArray(1, ATT_OP_EXECUTE_WRITE_RESPONSE)); + foreach (const QLowEnergyCharacteristic &characteristic, characteristics) + emit characteristic.d_ptr->characteristicChanged(characteristic, characteristic.value()); + foreach (const QLowEnergyDescriptor &descriptor, descriptors) + emit descriptor.d_ptr->descriptorWritten(descriptor, descriptor.value()); } void QLowEnergyControllerPrivate::sendErrorResponse(quint8 request, quint16 handle, quint8 code) @@ -2388,6 +2569,40 @@ void QLowEnergyControllerPrivate::sendListResponse(const QByteArray &packetStart sendPacket(response); } +void QLowEnergyControllerPrivate::sendNotification(QLowEnergyHandle handle) +{ + sendNotificationOrIndication(ATT_OP_HANDLE_VAL_NOTIFICATION, handle); +} + +void QLowEnergyControllerPrivate::sendIndication(QLowEnergyHandle handle) +{ + Q_ASSERT(!indicationInFlight); + indicationInFlight = true; + sendNotificationOrIndication(ATT_OP_HANDLE_VAL_INDICATION, handle); +} + +void QLowEnergyControllerPrivate::sendNotificationOrIndication( + quint8 opCode, + QLowEnergyHandle handle) +{ + Q_ASSERT(handle <= lastLocalHandle); + const Attribute &attribute = localAttributes.at(handle); + const int maxValueLength = qMin(attribute.value.count(), mtuSize - 3); + QByteArray packet(3 + maxValueLength, Qt::Uninitialized); + packet[0] = opCode; + putBtData(handle, packet.data() + 1); + using namespace std; + memcpy(packet.data() + 3, attribute.value.constData(), maxValueLength); + qCDebug(QT_BT_BLUEZ) << "sending notification/indication:" << packet.toHex(); + sendPacket(packet); +} + +void QLowEnergyControllerPrivate::sendNextIndication() +{ + if (!scheduledIndications.isEmpty()) + sendIndication(scheduledIndications.takeFirst()); +} + void QLowEnergyControllerPrivate::handleConnectionRequest() { if (state != QLowEnergyController::AdvertisingState) { @@ -2418,10 +2633,10 @@ void QLowEnergyControllerPrivate::handleConnectionRequest() connect(l2cpSocket, &QIODevice::readyRead, this, &QLowEnergyControllerPrivate::l2cpReadyRead); l2cpSocket->d_ptr->lowEnergySocketType = addressType == QLowEnergyController::PublicAddress ? BDADDR_LE_PUBLIC : BDADDR_LE_RANDOM; - l2cpSocket->setSocketDescriptor(clientSocket, QBluetoothServiceInfo::L2capProtocol); + l2cpSocket->setSocketDescriptor(clientSocket, QBluetoothServiceInfo::L2capProtocol, + QBluetoothSocket::ConnectedState, QIODevice::ReadWrite | QIODevice::Unbuffered); + restoreClientConfigurations(); setState(QLowEnergyController::ConnectedState); - // TODO: Send notifications and indications if this is a bonded device. - // The latter need to be queued. } void QLowEnergyControllerPrivate::closeServerSocket() @@ -2434,6 +2649,88 @@ void QLowEnergyControllerPrivate::closeServerSocket() serverSocketNotifier = nullptr; } +bool QLowEnergyControllerPrivate::isBonded() const +{ + // Pairing does not necessarily imply bonding, but we don't know whether the + // bonding flag was set in the original pairing request. + return QBluetoothLocalDevice(localAdapter).pairingStatus(remoteDevice) + != QBluetoothLocalDevice::Unpaired; +} + +QVector<QLowEnergyControllerPrivate::TempClientConfigurationData> QLowEnergyControllerPrivate::gatherClientConfigData() +{ + QVector<TempClientConfigurationData> data; + foreach (const auto &service, localServices) { + for (auto charIt = service->characteristicList.begin(); + charIt != service->characteristicList.end(); ++charIt) { + QLowEnergyServicePrivate::CharData &charData = charIt.value(); + for (auto descIt = charData.descriptorList.begin(); + descIt != charData.descriptorList.end(); ++descIt) { + QLowEnergyServicePrivate::DescData &descData = descIt.value(); + if (descData.uuid == QBluetoothUuid::ClientCharacteristicConfiguration) { + data << TempClientConfigurationData(&descData, charData.valueHandle, + descIt.key()); + break; + } + } + } + } + return data; +} + +void QLowEnergyControllerPrivate::storeClientConfigurations() +{ + if (!isBonded()) { + clientConfigData.remove(remoteDevice.toUInt64()); + return; + } + QVector<ClientConfigurationData> clientConfigs; + const QVector<TempClientConfigurationData> &tempConfigList = gatherClientConfigData(); + foreach (const auto &tempConfigData, tempConfigList) { + Q_ASSERT(tempConfigData.descData->value.count() == 2); + const quint16 value = bt_get_le16(tempConfigData.descData->value.constData()); + if (value != 0) { + clientConfigs << ClientConfigurationData(tempConfigData.charValueHandle, + tempConfigData.configHandle, value); + } + } + clientConfigData.insert(remoteDevice.toUInt64(), clientConfigs); +} + +void QLowEnergyControllerPrivate::restoreClientConfigurations() +{ + const QVector<TempClientConfigurationData> &tempConfigList = gatherClientConfigData(); + const QVector<ClientConfigurationData> &restoredClientConfigs = isBonded() + ? clientConfigData.value(remoteDevice.toUInt64()) : QVector<ClientConfigurationData>(); + QVector<QLowEnergyHandle> notifications; + foreach (const auto &tempConfigData, tempConfigList) { + bool wasRestored = false; + foreach (const auto &restoredData, restoredClientConfigs) { + if (restoredData.charValueHandle == tempConfigData.charValueHandle) { + Q_ASSERT(tempConfigData.descData->value.count() == 2); + putBtData(restoredData.configValue, tempConfigData.descData->value.data()); + wasRestored = true; + if (restoredData.charValueWasUpdated) { + if (isNotificationEnabled(restoredData.configValue)) + notifications << restoredData.charValueHandle; + else if (isIndicationEnabled(restoredData.configValue)) + scheduledIndications << restoredData.charValueHandle; + } + break; + } + } + if (!wasRestored) + tempConfigData.descData->value = QByteArray(2, 0); // Default value. + Q_ASSERT(lastLocalHandle >= tempConfigData.configHandle); + Q_ASSERT(tempConfigData.configHandle > tempConfigData.charValueHandle); + localAttributes[tempConfigData.configHandle].value = tempConfigData.descData->value; + } + + foreach (const QLowEnergyHandle handle, notifications) + sendNotification(handle); + sendNextIndication(); +} + static QByteArray uuidToByteArray(const QBluetoothUuid &uuid) { QByteArray ba; @@ -2498,6 +2795,8 @@ void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServ attribute.readConstraints = cd.readConstraints(); attribute.writeConstraints = cd.writeConstraints(); attribute.value = cd.value(); + attribute.minLength = cd.minimumValueLength(); + attribute.maxLength = cd.maximumValueLength(); localAttributes[attribute.handle] = attribute; foreach (const QLowEnergyDescriptorData &dd, cd.descriptors()) { @@ -2507,11 +2806,19 @@ void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServ attribute.properties = QLowEnergyCharacteristic::PropertyTypes(); attribute.readConstraints = AttAccessConstraints(); attribute.writeConstraints = AttAccessConstraints(); + attribute.minLength = 0; + attribute.maxLength = INT_MAX; + // Spec v4.2, Vol. 3, Part G, 3.3.3.x - if (attribute.type == QBluetoothUuid::CharacteristicExtendedProperties - || attribute.type == QBluetoothUuid::CharacteristicPresentationFormat - || attribute.type == QBluetoothUuid::CharacteristicAggregateFormat) { + if (attribute.type == QBluetoothUuid::CharacteristicExtendedProperties) { attribute.properties = QLowEnergyCharacteristic::Read; + attribute.minLength = attribute.maxLength = 2; + } else if (attribute.type == QBluetoothUuid::CharacteristicPresentationFormat) { + attribute.properties = QLowEnergyCharacteristic::Read; + attribute.minLength = attribute.maxLength = 7; + } else if (attribute.type == QBluetoothUuid::CharacteristicAggregateFormat) { + attribute.properties = QLowEnergyCharacteristic::Read; + attribute.minLength = 4; } else if (attribute.type == QBluetoothUuid::ClientCharacteristicConfiguration || attribute.type == QBluetoothUuid::ServerCharacteristicConfiguration) { attribute.properties = QLowEnergyCharacteristic::Read @@ -2519,6 +2826,7 @@ void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServ | QLowEnergyCharacteristic::WriteNoResponse | QLowEnergyCharacteristic::WriteSigned; attribute.writeConstraints = dd.writeConstraints(); + attribute.minLength = attribute.maxLength = 2; } else { if (dd.isReadable()) attribute.properties |= QLowEnergyCharacteristic::Read; @@ -2532,6 +2840,13 @@ void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServ } attribute.value = dd.value(); + if (attribute.value.count() < attribute.minLength + || attribute.value.count() > attribute.maxLength) { + qCWarning(QT_BT_BLUEZ) << "attribute of type" << attribute.type + << "has invalid length of" << attribute.value.count() + << "bytes"; + attribute.value = QByteArray(attribute.minLength, 0); + } localAttributes[attribute.handle] = attribute; } } diff --git a/src/bluetooth/qlowenergycontroller_p.h b/src/bluetooth/qlowenergycontroller_p.h index e693e84d..f3b28f5c 100644 --- a/src/bluetooth/qlowenergycontroller_p.h +++ b/src/bluetooth/qlowenergycontroller_p.h @@ -183,6 +183,8 @@ public: QBluetooth::AttAccessConstraints writeConstraints; QBluetoothUuid type; QByteArray value; + int minLength; + int maxLength; }; QVector<Attribute> localAttributes; @@ -211,6 +213,32 @@ private: }; QVector<WriteRequest> openPrepareWriteRequests; + // Invariant: !scheduledIndications.isEmpty => indicationInFlight == true + QVector<QLowEnergyHandle> scheduledIndications; + bool indicationInFlight = false; + + struct TempClientConfigurationData { + TempClientConfigurationData(QLowEnergyServicePrivate::DescData *dd = nullptr, + QLowEnergyHandle chHndl = 0, QLowEnergyHandle coHndl = 0) + : descData(dd), charValueHandle(chHndl), configHandle(coHndl) {} + + QLowEnergyServicePrivate::DescData *descData; + QLowEnergyHandle charValueHandle; + QLowEnergyHandle configHandle; + }; + + struct ClientConfigurationData { + ClientConfigurationData(QLowEnergyHandle chHndl = 0, QLowEnergyHandle coHndl = 0, + quint16 val = 0) + : charValueHandle(chHndl), configHandle(coHndl), configValue(val) {} + + QLowEnergyHandle charValueHandle; + QLowEnergyHandle configHandle; + quint16 configValue; + bool charValueWasUpdated = false; + }; + QHash<quint64, QVector<ClientConfigurationData>> clientConfigData; + bool requestPending; quint16 mtuSize; int securityLevelValue; @@ -224,6 +252,11 @@ private: void handleConnectionRequest(); void closeServerSocket(); + bool isBonded() const; + QVector<TempClientConfigurationData> gatherClientConfigData(); + void storeClientConfigurations(); + void restoreClientConfigurations(); + void sendPacket(const QByteArray &packet); void sendNextPendingRequest(); void processReply(const Request &request, const QByteArray &reply); @@ -280,6 +313,11 @@ private: void sendListResponse(const QByteArray &packetStart, int elemSize, const QVector<Attribute> &attributes, const ElemWriter &elemWriter); + void sendNotification(QLowEnergyHandle handle); + void sendIndication(QLowEnergyHandle handle); + void sendNotificationOrIndication(quint8 opCode, QLowEnergyHandle handle); + void sendNextIndication(); + void ensureUniformAttributes(QVector<Attribute> &attributes, const std::function<int(const Attribute &)> &getSize); void ensureUniformUuidSizes(QVector<Attribute> &attributes); void ensureUniformValueSizes(QVector<Attribute> &attributes); @@ -292,6 +330,31 @@ private: int checkReadPermissions(const Attribute &attr); int checkReadPermissions(QVector<Attribute> &attributes); + void updateLocalAttributeValue( + QLowEnergyHandle handle, + const QByteArray &value, + QLowEnergyCharacteristic &characteristic, + QLowEnergyDescriptor &descriptor); + + void writeCharacteristicForPeripheral( + QLowEnergyServicePrivate::CharData &charData, + const QByteArray &newValue); + void writeCharacteristicForCentral( + QLowEnergyHandle charHandle, + QLowEnergyHandle valueHandle, + const QByteArray &newValue, + bool writeWithResponse); + + void writeDescriptorForPeripheral( + const QSharedPointer<QLowEnergyServicePrivate> &service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle, + const QByteArray &newValue); + void writeDescriptorForCentral( + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle, + const QByteArray &newValue); + private slots: void l2cpConnected(); void l2cpDisconnected(); diff --git a/src/bluetooth/qlowenergyservice.cpp b/src/bluetooth/qlowenergyservice.cpp index 9cccbc93..a05ba4ec 100644 --- a/src/bluetooth/qlowenergyservice.cpp +++ b/src/bluetooth/qlowenergyservice.cpp @@ -220,6 +220,9 @@ QT_BEGIN_NAMESPACE \l serviceUuid() and \l serviceName(). \value DiscoveringServices The service details are being discovered. \value ServiceDiscovered The service details have been discovered. + \value LocalService The service is associated with a controller object in the + \l{QLowEnergyController::PeripheralRole}{peripheral role}. Such + service objects do not change their state. */ /*! @@ -302,15 +305,20 @@ QT_BEGIN_NAMESPACE /*! \fn void QLowEnergyService::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) - This signal is emitted when the value of \a characteristic is changed - by an event on the peripheral. The \a newValue parameter contains the - updated value of the \a characteristic. - - The signal emission implies that change notifications must + If the associated controller object is in the \l {QLowEnergyController::CentralRole}{central} + role, this signal is emitted when the value of \a characteristic is changed by an event on the + peripheral. In that case, the signal emission implies that change notifications must have been activated via the characteristic's \l {QBluetoothUuid::ClientCharacteristicConfiguration}{ClientCharacteristicConfiguration} descriptor prior to the change event on the peripheral. More details on how this might be done can be found further \l{notifications}{above}. + + If the controller is in the \l {QLowEnergyController::PeripheralRole}{peripheral} role, that is, + the service object was created via \l QLowEnergyController::addService, the signal is emitted + when a GATT client has written the value of the characteristic using a write request or command. + + The \a newValue parameter contains the updated value of the \a characteristic. + */ /*! @@ -329,8 +337,10 @@ QT_BEGIN_NAMESPACE \fn void QLowEnergyService::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) This signal is emitted when the value of \a descriptor - is successfully changed to \a newValue. The change must have been caused - by calling \l writeDescriptor(). + is successfully changed to \a newValue. If the associated controller object is in the + \l {QLowEnergyController::CentralRole}{central} role, the change must have been caused + by calling \l writeDescriptor(). Otherwise, the signal is the result of a write request or + command from a GATT client to the respective descriptor. \sa writeDescriptor() */ @@ -611,7 +621,13 @@ void QLowEnergyService::readCharacteristic( } /*! - Writes \a newValue as value for the \a characteristic. If the operation is successful, + Writes \a newValue as value for the \a characteristic. The exact semantics depend on + the role that the associated controller object is in. + + \b {Central role} + + The call results in a write request or command to a remote peripheral. + If the operation is successful, the \l characteristicWritten() signal is emitted; otherwise the \l CharacteristicWriteError is set. @@ -640,6 +656,21 @@ void QLowEnergyService::readCharacteristic( characteristic may only support \l WriteWithResponse. If the hardware returns with an error the \l CharacteristicWriteError is set. + \b {Peripheral role} + + The call results in the value of the characteristic getting updated in the local database. + + If a client is currently connected and it has enabled notifications or indications for + the characteristic, the respective information will be sent. + If a device has enabled notifications or indications for the characteristic and that device + is currently not connected, but a bond exists between it and the local device, then + the notification or indication will be sent on the next reconnection. + + If there is a constraint on the length of the characteristic value and \a newValue + does not adhere to that constraint, the behavior is unspecified. + + \note The \a mode argument is ignored in peripheral mode. + \sa QLowEnergyService::characteristicWritten(), QLowEnergyService::readCharacteristic() */ @@ -650,7 +681,10 @@ void QLowEnergyService::writeCharacteristic( //TODO check behavior when writing to WriteSigned characteristic Q_D(QLowEnergyService); - if (d->controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(characteristic)) { + if (d->controller == Q_NULLPTR + || (d->controller->role == QLowEnergyController::CentralRole + && state() != ServiceDiscovered) + || !contains(characteristic)) { d->setError(QLowEnergyService::OperationError); return; } @@ -727,9 +761,14 @@ void QLowEnergyService::readDescriptor( } /*! - Writes \a newValue as value for \a descriptor. If the operation is successful, - the \l descriptorWritten() signal is emitted; otherwise the \l DescriptorWriteError - is emitted. + Writes \a newValue as value for \a descriptor. The exact semantics depend on + the role that the associated controller object is in. + + \b {Central role} + + A call to this function results in a write request to the remote device. + If the operation is successful, the \l descriptorWritten() signal is emitted; otherwise + the \l DescriptorWriteError is emitted. All descriptor and characteristic requests towards the same remote device are serialised. A queue is employed when issuing multiple write requests at the same time. @@ -741,6 +780,11 @@ void QLowEnergyService::readDescriptor( belongs to the service. If one of these conditions is not true the \l QLowEnergyService::OperationError is set. + \b {Peripheral Role} + + The value is written to the local service database. If the contents of \a newValue are not + valid for \a descriptor, the behavior is unspecified. + \sa descriptorWritten(), readDescriptor() */ void QLowEnergyService::writeDescriptor(const QLowEnergyDescriptor &descriptor, @@ -748,7 +792,10 @@ void QLowEnergyService::writeDescriptor(const QLowEnergyDescriptor &descriptor, { Q_D(QLowEnergyService); - if (d->controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(descriptor)) { + if (d->controller == Q_NULLPTR + || (d->controller->role == QLowEnergyController::CentralRole + && state() != ServiceDiscovered) + || !contains(descriptor)) { d->setError(QLowEnergyService::OperationError); return; } diff --git a/src/bluetooth/qlowenergyservice.h b/src/bluetooth/qlowenergyservice.h index 1dd35ab1..6e65aefd 100644 --- a/src/bluetooth/qlowenergyservice.h +++ b/src/bluetooth/qlowenergyservice.h @@ -69,7 +69,8 @@ public: DiscoveryRequired, // we know start/end handle but nothing more //TODO Rename DiscoveringServices -> DiscoveringDetails or DiscoveringService DiscoveringServices,// discoverDetails() called and running - ServiceDiscovered // all details have been synchronized + ServiceDiscovered, // all details have been synchronized + LocalService, }; Q_ENUM(ServiceState) diff --git a/src/bluetooth/qlowenergyserviceprivate.cpp b/src/bluetooth/qlowenergyserviceprivate.cpp index 6f112017..a29d5648 100644 --- a/src/bluetooth/qlowenergyserviceprivate.cpp +++ b/src/bluetooth/qlowenergyserviceprivate.cpp @@ -33,6 +33,8 @@ #include "qlowenergyserviceprivate_p.h" +#include "qlowenergycontroller_p.h" + QT_BEGIN_NAMESPACE QLowEnergyServicePrivate::QLowEnergyServicePrivate(QObject *parent) : diff --git a/src/bluetooth/qlowenergyserviceprivate_p.h b/src/bluetooth/qlowenergyserviceprivate_p.h index fc2276e6..c17932cf 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -51,10 +51,10 @@ #include <QtBluetooth/QLowEnergyService> #include <QtBluetooth/QLowEnergyCharacteristic> -#include "qlowenergycontroller_p.h" - QT_BEGIN_NAMESPACE +class QLowEnergyControllerPrivate; + class QLowEnergyServicePrivate : public QObject { Q_OBJECT diff --git a/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp index b1fc7256..97adf9db 100644 --- a/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp +++ b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp @@ -48,6 +48,9 @@ static QByteArray deviceName() { return "Qt GATT server"; } static QScopedPointer<QLowEnergyController> leController; typedef QSharedPointer<QLowEnergyService> ServicePtr; static QHash<QBluetoothUuid, ServicePtr> services; +static int descriptorWriteCount = 0; +static int disconnectCount = 0; +static QBluetoothAddress remoteDevice; void addService(const QLowEnergyServiceData &serviceData) { @@ -64,7 +67,7 @@ void addRunningSpeedService() QLowEnergyDescriptorData desc; desc.setUuid(QBluetoothUuid::ClientCharacteristicConfiguration); - desc.setValue(QByteArray(1, 0)); // Default: No indication, no notification. + desc.setValue(QByteArray(2, 0)); // Default: No indication, no notification. QLowEnergyCharacteristicData charData; charData.setUuid(QBluetoothUuid::RSCMeasurement); charData.addDescriptor(desc); @@ -111,7 +114,7 @@ void addGenericAccessService() void addCustomService() { QLowEnergyServiceData serviceData; - serviceData.setUuid(QBluetoothUuid(quint16(0x2000))); // Made up. + serviceData.setUuid(QBluetoothUuid(quint16(0x2000))); serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); QLowEnergyCharacteristicData charData; @@ -120,12 +123,24 @@ void addCustomService() charData.setValue(QByteArray(1024, 'x')); // Long value to test "Read Blob". serviceData.addCharacteristic(charData); - charData.setUuid(QBluetoothUuid(quint16(0x5001))); // Made up. + charData.setUuid(QBluetoothUuid(quint16(0x5001))); charData.setProperties(QLowEnergyCharacteristic::Read); charData.setReadConstraints(QBluetooth::AttAuthorizationRequired); // To test read failure. serviceData.addCharacteristic(charData); charData.setValue("something"); + charData.setUuid(QBluetoothUuid(quint16(0x5002))); + charData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Indicate); + charData.setReadConstraints(QBluetooth::AttAccessConstraints()); + const QLowEnergyDescriptorData desc(QBluetoothUuid::ClientCharacteristicConfiguration, + QByteArray(2, 0)); + charData.addDescriptor(desc); + serviceData.addCharacteristic(charData); + + charData.setUuid(QBluetoothUuid(quint16(0x5003))); + charData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify); + serviceData.addCharacteristic(charData); + addService(serviceData); } @@ -150,8 +165,55 @@ int main(int argc, char *argv[]) addCustomService(); startAdvertising(); - // TODO: Change characteristics, client checks that it gets indication/notification - // TODO: Where to test that we get the characteristicChanged signal for characteristics that the client writes? + const ServicePtr customService = services.value(QBluetoothUuid(quint16(0x2000))); + Q_ASSERT(customService); + + const auto stateChangedHandler = [customService]() { + switch (leController->state()) { + case QLowEnergyController::ConnectedState: + remoteDevice = leController->remoteAddress(); + break; + case QLowEnergyController::UnconnectedState: { + if (++disconnectCount == 2) { + qApp->quit(); + break; + } + Q_ASSERT(disconnectCount == 1); + const QLowEnergyCharacteristic indicatableChar + = customService->characteristic(QBluetoothUuid(quint16(0x5002))); + Q_ASSERT(indicatableChar.isValid()); + customService->writeCharacteristic(indicatableChar, "indicated2"); + Q_ASSERT(indicatableChar.value() == "indicated2"); + const QLowEnergyCharacteristic notifiableChar + = customService->characteristic(QBluetoothUuid(quint16(0x5003))); + Q_ASSERT(notifiableChar.isValid()); + customService->writeCharacteristic(notifiableChar, "notified2"); + Q_ASSERT(notifiableChar.value() == "notified2"); + startAdvertising(); + break; + } + default: + break; + } + }; + + QObject::connect(leController.data(), &QLowEnergyController::stateChanged, stateChangedHandler); + const auto descriptorWriteHandler = [customService]() { + if (++descriptorWriteCount != 2) + return; + const QLowEnergyCharacteristic indicatableChar + = customService->characteristic(QBluetoothUuid(quint16(0x5002))); + Q_ASSERT(indicatableChar.isValid()); + customService->writeCharacteristic(indicatableChar, "indicated"); + Q_ASSERT(indicatableChar.value() == "indicated"); + const QLowEnergyCharacteristic notifiableChar + = customService->characteristic(QBluetoothUuid(quint16(0x5003))); + Q_ASSERT(notifiableChar.isValid()); + customService->writeCharacteristic(notifiableChar, "notified"); + Q_ASSERT(notifiableChar.value() == "notified"); + }; + QObject::connect(customService.data(), &QLowEnergyService::descriptorWritten, + descriptorWriteHandler); return app.exec(); } diff --git a/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp index 25098273..c1701217 100644 --- a/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp +++ b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp @@ -34,6 +34,7 @@ #include <QtBluetooth/qbluetoothaddress.h> #include <QtBluetooth/qbluetoothdevicediscoveryagent.h> #include <QtBluetooth/qbluetoothdeviceinfo.h> +#include <QtBluetooth/qbluetoothlocaldevice.h> #include <QtBluetooth/qlowenergyadvertisingdata.h> #include <QtBluetooth/qlowenergyadvertisingparameters.h> #include <QtBluetooth/qlowenergycontroller.h> @@ -63,7 +64,7 @@ private slots: // Interaction with actual GATT server goes here. Order is relevant. void advertisedData(); - void initialServices(); + void serverCommunication(); private: QBluetoothAddress m_serverAddress; @@ -171,8 +172,15 @@ void TestQLowEnergyControllerGattServer::advertisedData() QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid(quint16(0x2000)))); } -void TestQLowEnergyControllerGattServer::initialServices() +// TODO: Why on earth is this not in the library??? +Q_DECLARE_METATYPE(QLowEnergyCharacteristic) +Q_DECLARE_METATYPE(QLowEnergyDescriptor) + +void TestQLowEnergyControllerGattServer::serverCommunication() { + qRegisterMetaType<QLowEnergyCharacteristic>(); + qRegisterMetaType<QLowEnergyDescriptor>(); + if (m_serverAddress.isNull()) QSKIP("No server address provided"); m_leController.reset(QLowEnergyController::createCentral(m_serverInfo)); @@ -235,7 +243,7 @@ void TestQLowEnergyControllerGattServer::initialServices() const QLowEnergyDescriptor clientConfigDesc = measurementChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); QVERIFY(clientConfigDesc.isValid()); - QCOMPARE(clientConfigDesc.value(), QByteArray(1, 0)); + QCOMPARE(clientConfigDesc.value(), QByteArray(2, 0)); QCOMPARE(measurementChar.properties(), QLowEnergyCharacteristic::Notify); QCOMPARE(measurementChar.value(), QByteArray()); // Empty because Read property not set QLowEnergyCharacteristic featureChar @@ -247,16 +255,16 @@ void TestQLowEnergyControllerGattServer::initialServices() featureValue[0] = 1 << 2; QCOMPARE(featureChar.value(), featureValue); - const QScopedPointer<QLowEnergyService> customService( + QScopedPointer<QLowEnergyService> customService( m_leController->createServiceObject(QBluetoothUuid(quint16(0x2000)))); QVERIFY(!customService.isNull()); customService->discoverDetails(); while (customService->state() != QLowEnergyService::ServiceDiscovered) { spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::stateChanged)); - QVERIFY(spy->wait(3000)); + QVERIFY(spy->wait(5000)); } QCOMPARE(customService->includedServices().count(), 0); - QCOMPARE(customService->characteristics().count(), 2); + QCOMPARE(customService->characteristics().count(), 4); QLowEnergyCharacteristic customChar = customService->characteristic(QBluetoothUuid(quint16(0x5000))); QVERIFY(customChar.isValid()); @@ -269,11 +277,99 @@ void TestQLowEnergyControllerGattServer::initialServices() QCOMPARE(customChar2.descriptors().count(), 0); QCOMPARE(customChar2.value(), QByteArray()); // Was not readable due to authorization requirement. + QLowEnergyCharacteristic customChar3 + = customService->characteristic(QBluetoothUuid(quint16(0x5002))); + QVERIFY(customChar3.isValid()); + QCOMPARE(customChar3.properties(), + QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Indicate); + QCOMPARE(customChar3.descriptors().count(), 1); + QLowEnergyDescriptor cc3ClientConfig + = customChar3.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + QVERIFY(cc3ClientConfig.isValid()); + + QLowEnergyCharacteristic customChar4 + = customService->characteristic(QBluetoothUuid(quint16(0x5003))); + QVERIFY(customChar4.isValid()); + QCOMPARE(customChar4.properties(), + QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify); + QCOMPARE(customChar4.descriptors().count(), 1); + QLowEnergyDescriptor cc4ClientConfig + = customChar4.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + QVERIFY(cc4ClientConfig.isValid()); + customService->writeCharacteristic(customChar, "whatever"); spy.reset(new QSignalSpy(customService.data(), static_cast<void (QLowEnergyService::*) (QLowEnergyService::ServiceError)>(&QLowEnergyService::error))); QVERIFY(spy->wait(3000)); QCOMPARE(customService->error(), QLowEnergyService::CharacteristicWriteError); + + QByteArray indicateValue(2, 0); + indicateValue[0] = 2; + customService->writeDescriptor(cc3ClientConfig, indicateValue); + spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::descriptorWritten)); + QVERIFY(spy->wait(3000)); + + QByteArray notifyValue(2, 0); + notifyValue[0] = 1; + customService->writeDescriptor(cc4ClientConfig, notifyValue); + spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::descriptorWritten)); + QVERIFY(spy->wait(3000)); + + // Server now changes the characteristic values. + + spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::characteristicChanged)); + QVERIFY(spy->wait(3000)); + if (spy->count() == 1) + QVERIFY(spy->wait(3000)); + QCOMPARE(customChar3.value().constData(), "indicated"); + QCOMPARE(customChar4.value().constData(), "notified"); + + const bool isBonded = QBluetoothLocalDevice().pairingStatus(m_serverAddress) + != QBluetoothLocalDevice::Unpaired; + m_leController->disconnectFromDevice(); + + if (m_leController->state() != QLowEnergyController::UnconnectedState) { + spy.reset(new QSignalSpy(m_leController.data(), &QLowEnergyController::stateChanged)); + QVERIFY(spy->wait(3000)); + } + QCOMPARE(m_leController->state(), QLowEnergyController::UnconnectedState); + + // Server now changes the characteristic values again while we're offline. + // Note: We cannot test indications and notifications for this case, as the client does + // not cache the old information and thus does not yet know the characteristics + // at the time the notification/indication is received. + + QTest::qWait(3000); + m_leController->connectToDevice(); + spy.reset(new QSignalSpy(m_leController.data(), &QLowEnergyController::connected)); + QVERIFY(spy->wait(30000)); + m_leController->discoverServices(); + spy.reset(new QSignalSpy(m_leController.data(), &QLowEnergyController::discoveryFinished)); + QVERIFY(spy->wait(30000)); + customService.reset(m_leController->createServiceObject(QBluetoothUuid(quint16(0x2000)))); + QVERIFY(!customService.isNull()); + customService->discoverDetails(); + while (customService->state() != QLowEnergyService::ServiceDiscovered) { + spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::stateChanged)); + QVERIFY(spy->wait(5000)); + } + customChar3 = customService->characteristic(QBluetoothUuid(quint16(0x5002))); + QVERIFY(customChar3.isValid()); + QCOMPARE(customChar3.value().constData(), "indicated2"); + customChar4 = customService->characteristic(QBluetoothUuid(quint16(0x5003))); + QVERIFY(customChar4.isValid()); + QCOMPARE(customChar4.value().constData(), "notified2"); + cc3ClientConfig = customChar3.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + QVERIFY(cc3ClientConfig.isValid()); + cc4ClientConfig = customChar4.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + QVERIFY(cc4ClientConfig.isValid()); + if (isBonded) { + QCOMPARE(cc3ClientConfig.value(), indicateValue); + QCOMPARE(cc4ClientConfig.value(), notifyValue); + } else { + QCOMPARE(cc3ClientConfig.value(), QByteArray(2, 0)); + QCOMPARE(cc4ClientConfig.value(), QByteArray(2, 0)); + } } void TestQLowEnergyControllerGattServer::controllerType() @@ -320,6 +416,13 @@ void TestQLowEnergyControllerGattServer::serviceData() charData.setValue("value"); QCOMPARE(charData.value().constData(), "value"); + charData.setValueLength(4, 7); + QCOMPARE(charData.minimumValueLength(), 4); + QCOMPARE(charData.maximumValueLength(), 7); + charData.setValueLength(5, 2); + QCOMPARE(charData.minimumValueLength(), 5); + QCOMPARE(charData.maximumValueLength(), 5); + const QLowEnergyCharacteristic::PropertyTypes props = QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::WriteSigned; charData.setProperties(props); |