summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/bluetooth/qlowenergycharacteristicdata.cpp43
-rw-r--r--src/bluetooth/qlowenergycharacteristicdata.h4
-rw-r--r--src/bluetooth/qlowenergycontroller.cpp2
-rw-r--r--src/bluetooth/qlowenergycontroller_bluez.cpp507
-rw-r--r--src/bluetooth/qlowenergycontroller_p.h63
-rw-r--r--src/bluetooth/qlowenergyservice.cpp73
-rw-r--r--src/bluetooth/qlowenergyservice.h3
-rw-r--r--src/bluetooth/qlowenergyserviceprivate.cpp2
-rw-r--r--src/bluetooth/qlowenergyserviceprivate_p.h4
-rw-r--r--tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp72
-rw-r--r--tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp115
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);