diff options
49 files changed, 5926 insertions, 179 deletions
diff --git a/examples/bluetooth/bluetooth.pro b/examples/bluetooth/bluetooth.pro index a98bb62d..1c09dade 100644 --- a/examples/bluetooth/bluetooth.pro +++ b/examples/bluetooth/bluetooth.pro @@ -1,4 +1,7 @@ TEMPLATE = subdirs + +SUBDIRS += heartrate-server + qtHaveModule(widgets) { SUBDIRS += btchat \ btscanner \ diff --git a/examples/bluetooth/heartlistener/doc/src/heartlistener.qdoc b/examples/bluetooth/heartlistener/doc/src/heartlistener.qdoc index e7e1176f..2bb59ae1 100644 --- a/examples/bluetooth/heartlistener/doc/src/heartlistener.qdoc +++ b/examples/bluetooth/heartlistener/doc/src/heartlistener.qdoc @@ -51,8 +51,10 @@ A Bluetooth Low Energy device with a Heart Rate service is required for this application to work. An alternative might be a programmable - Bluetooth Low Energy device which might simulate the service. If no such device can be - found, the example uses a demo mode which creates and displays random values. + Bluetooth Low Energy device which might simulate the service. You can also use the + \l {heartrate-server} {Heart Rate server} example for that purpose. + If no such device can be found, the example uses a demo mode which creates and displays + random values. The \l {lowenergyscanner}{Bluetooth Low Energy Scanner} example might be more suitable if a heart rate device is not available. The scanner example works with any type of Bluetooth diff --git a/examples/bluetooth/heartrate-server/doc/src/heartrate-server.qdoc b/examples/bluetooth/heartrate-server/doc/src/heartrate-server.qdoc new file mode 100644 index 00000000..22c6fcd1 --- /dev/null +++ b/examples/bluetooth/heartrate-server/doc/src/heartrate-server.qdoc @@ -0,0 +1,115 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example heartrate-server + \title Bluetooth Low Energy Heart Rate Server Example + \brief An example demonstrating how to set up and advertise a GATT service. The example + demonstrates the use of the Qt Bluetooth Low Energy classes related to peripheral (slave) + functionality. + + The Bluetooth Low Energy Heart Rate Server is a command-line application that shows how to + develop a Bluetooth GATT server using the Qt Bluetooth API. + The application covers setting up a service, advertising it and notifying clients about changes + to characteristic values. + + The example makes use of the following Qt classes: + + \list + \li \l QLowEnergyAdvertisingData + \li \l QLowEnergyAdvertisingParameters + \li \l QLowEnergyServiceData + \li \l QLowEnergyCharacteristicData + \li \l QLowEnergyDescriptorData + \li \l QLowEnergyController + \li \l QLowEnergyService + + \endlist + + The example implements a server application, which means it has no graphical user interface. + To visualize what it is doing, you can use the \l {heartlistener}{Heart Listener} + example, which is basically the client-side counterpart to this application. + + \note On Linux, advertising requires privileged access, so you need to run + the example as root, for instance via \c sudo. + + \section1 Setting up Advertising Data and Parameters + Two classes are used to configure the advertising process: \l QLowEnergyAdvertisingData to + specify which information is to be broadcast, and \l QLowEnergyAdvertisingParameters for + specific aspects such as setting the advertising interval or controlling which devices are + allowed to connect. In our example, we simply use the default parameters. + + The information contained in the \l QLowEnergyAdvertisingData will be visible to other devices + that are currently scanning. They can use it to decide whether they want to establish a connection + or not. In our example, we include the type of service we offer, a name that adequately + describes our device to humans, and the transmit power level of the device. The latter is + often useful to potential clients, because they can tell how far away our device is by + comparing the received signal strength to the advertised one. + \note Space for the advertising data is very limited (only 31 bytes in total), so + variable-length data such as the device name should be kept as short as possible. + \snippet heartrate-server/main.cpp Advertising Data + + \section1 Setting up Service Data + Next we configure the kind of service we want to offer. We use the \c {Heart Rate} service as + defined in the Bluetooth specification in its minimal form, that is, consisting only of the + \c {Heart Rate Measurement} characteristic. This characteristic must support the \c Notify + property (and no others), and it needs to have a \c {Client Characteristic Configuration} + descriptor, which enables clients to register to get notified about changes to characteristic + values. We set the initial heart rate value to zero, as it cannot be read anyway (the only + way the client can get at the value is via notifications). + \snippet heartrate-server/main.cpp Service Data + + \section1 Advertising and Listening for Incoming Connections + Now that all the data has been set up, we can start advertising. First we create a + \l QLowEnergyController object in the + \l {QLowEnergyController::PeripheralRole} {peripheral role} and use it to create a (dynamic) + \l QLowEnergyService object from our (static) \l QLowEnergyServiceData. + Then we call \l QLowEnergyController::startAdvertising(). + Note that we hand in our \l QLowEnergyAdvertisingData twice: The first argument + acts as the actual advertising data, the second one as the scan response data. They could + transport different information, but here we don't have a need for that. We also pass + a default-constructed instance of \l QLowEnergyAdvertisingParameters, because the default + advertising parameters are fine for us. If a client is interested in the advertised service, + it can now establish a connection to our device. When that happens, the device stops advertising + and the \l QLowEnergyController::connected() signal is emitted. + \note When a client disconnects, advertising does not resume automatically. If you want that + to happen, you need to connect to the \l QLowEnergyController::disconnected() signal + and call \l QLowEnergyController::startAdvertising() in the respective slot. + \snippet heartrate-server/main.cpp Start Advertising + + \section1 Providing the Heartrate + So far, so good. But how does a client actually get at the heart rate? This happens by + regularly updating the value of the respective characteristic in the \l QLowEnergyService + object that we received from the \l QLowEnergyController in the code snippet above. + The source of the heart rate would normally be some kind of sensor, but in our example, + we just make up values that we let oscillate between 60 and 100. The most important part in the + following code snippet is the call to \l QLowEnergyService::writeCharacteristic. If + a client is currently connected and has enabled notifications by writing to the aforementioned + \c {Client Characteristic Configuration}, it will get notified about the new value. + \snippet heartrate-server/main.cpp Provide Heartbeat +*/ + diff --git a/examples/bluetooth/heartrate-server/heartrate-server.pro b/examples/bluetooth/heartrate-server/heartrate-server.pro new file mode 100644 index 00000000..4ccf486c --- /dev/null +++ b/examples/bluetooth/heartrate-server/heartrate-server.pro @@ -0,0 +1,10 @@ +TEMPLATE = app +TARGET = heartrate-server + +QT += bluetooth +CONFIG += c++11 + +SOURCES += main.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/bluetooth/heartrate-server +INSTALLS += target diff --git a/examples/bluetooth/heartrate-server/main.cpp b/examples/bluetooth/heartrate-server/main.cpp new file mode 100644 index 00000000..779dbb6a --- /dev/null +++ b/examples/bluetooth/heartrate-server/main.cpp @@ -0,0 +1,121 @@ +/*************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtBluetooth/qlowenergyadvertisingdata.h> +#include <QtBluetooth/qlowenergyadvertisingparameters.h> +#include <QtBluetooth/qlowenergycharacteristic.h> +#include <QtBluetooth/qlowenergycharacteristicdata.h> +#include <QtBluetooth/qlowenergydescriptordata.h> +#include <QtBluetooth/qlowenergycontroller.h> +#include <QtBluetooth/qlowenergyservice.h> +#include <QtBluetooth/qlowenergyservicedata.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qlist.h> +#include <QtCore/qscopedpointer.h> +#include <QtCore/qtimer.h> + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + //! [Advertising Data] + QLowEnergyAdvertisingData advertisingData; + advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); + advertisingData.setIncludePowerLevel(true); + advertisingData.setLocalName("HeartRateServer"); + advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid::HeartRate); + //! [Advertising Data] + + //! [Service Data] + QLowEnergyCharacteristicData charData; + charData.setUuid(QBluetoothUuid::HeartRateMeasurement); + charData.setValue(QByteArray(2, 0)); + charData.setProperties(QLowEnergyCharacteristic::Notify); + const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::ClientCharacteristicConfiguration, + QByteArray(2, 0)); + charData.addDescriptor(clientConfig); + + QLowEnergyServiceData serviceData; + serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); + serviceData.setUuid(QBluetoothUuid::HeartRate); + serviceData.addCharacteristic(charData); + //! [Service Data] + + //! [Start Advertising] + const QScopedPointer<QLowEnergyController> leController(QLowEnergyController::createPeripheral()); + const QScopedPointer<QLowEnergyService> service(leController->addService(serviceData)); + leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData, + advertisingData); + //! [Start Advertising] + + //! [Provide Heartbeat] + QTimer heartbeatTimer; + quint8 currentHeartRate = 60; + enum ValueChange { ValueUp, ValueDown } valueChange = ValueUp; + const auto heartbeatProvider = [&service, ¤tHeartRate, &valueChange]() { + QByteArray value; + value.append(char(0)); // Flags that specify the format of the value. + value.append(char(currentHeartRate)); // Actual value. + QLowEnergyCharacteristic characteristic + = service->characteristic(QBluetoothUuid::HeartRateMeasurement); + Q_ASSERT(characteristic.isValid()); + service->writeCharacteristic(characteristic, value); // Potentially causes notification. + if (currentHeartRate == 60) + valueChange = ValueUp; + else if (currentHeartRate == 100) + valueChange = ValueDown; + if (valueChange == ValueUp) + ++currentHeartRate; + else + --currentHeartRate; + }; + QObject::connect(&heartbeatTimer, &QTimer::timeout, heartbeatProvider); + heartbeatTimer.start(1000); + //! [Provide Heartbeat] + + auto reconnect = [&leController, advertisingData]() { + leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData, + advertisingData); + }; + QObject::connect(leController.data(), &QLowEnergyController::disconnected, reconnect); + + return app.exec(); +} diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index 67a001d2..ef5337b5 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -24,9 +24,15 @@ PUBLIC_HEADERS += \ qbluetoothtransfermanager.h \ qbluetoothtransferrequest.h \ qlowenergyservice.h \ + qlowenergyservicedata.h \ qlowenergycharacteristic.h \ + qlowenergycharacteristicdata.h \ qlowenergydescriptor.h \ + qlowenergydescriptordata.h \ qbluetoothtransferreply.h \ + qlowenergyadvertisingdata.h \ + qlowenergyadvertisingparameters.h \ + qlowenergyconnectionparameters.h \ qlowenergycontroller.h PRIVATE_HEADERS += \ @@ -43,7 +49,8 @@ PRIVATE_HEADERS += \ qprivatelinearbuffer_p.h \ qbluetoothlocaldevice_p.h \ qlowenergycontroller_p.h \ - qlowenergyserviceprivate_p.h + qlowenergyserviceprivate_p.h \ + qleadvertiser_p.h \ SOURCES += \ qbluetoothaddress.cpp\ @@ -60,9 +67,15 @@ SOURCES += \ qbluetoothtransfermanager.cpp \ qbluetoothtransferrequest.cpp \ qbluetoothtransferreply.cpp \ + qlowenergyadvertisingdata.cpp \ + qlowenergyadvertisingparameters.cpp \ + qlowenergyconnectionparameters.cpp \ qlowenergyservice.cpp \ + qlowenergyservicedata.cpp \ qlowenergycharacteristic.cpp \ + qlowenergycharacteristicdata.cpp \ qlowenergydescriptor.cpp \ + qlowenergydescriptordata.cpp \ qlowenergycontroller.cpp \ qlowenergyserviceprivate.cpp @@ -88,6 +101,7 @@ config_bluez:qtHaveModule(dbus) { # old versions of Bluez do not have the required BTLE symbols config_bluez_le { SOURCES += \ + qleadvertiser_bluez.cpp \ qlowenergycontroller_bluez.cpp } else { message("Bluez version is too old to support Bluetooth Low Energy.") diff --git a/src/bluetooth/bluez/bluez_data_p.h b/src/bluetooth/bluez/bluez_data_p.h index 2fa5ccc2..e8d1ec62 100644 --- a/src/bluetooth/bluez/bluez_data_p.h +++ b/src/bluetooth/bluez/bluez_data_p.h @@ -140,22 +140,6 @@ struct sockaddr_rc { // Bt Low Energy related -#define bt_get_unaligned(ptr) \ -({ \ - struct __attribute__((packed)) { \ - __typeof__(*(ptr)) __v; \ - } *__p = (__typeof__(__p)) (ptr); \ - __p->__v; \ -}) - -#define bt_put_unaligned(val, ptr) \ -do { \ - struct __attribute__((packed)) { \ - __typeof__(*(ptr)) __v; \ - } *__p = (__typeof__(__p)) (ptr); \ - __p->__v = (val); \ -} while (0) - #if __BYTE_ORDER == __LITTLE_ENDIAN static inline void btoh128(const quint128 *src, quint128 *dst) @@ -171,15 +155,7 @@ static inline void ntoh128(const quint128 *src, quint128 *dst) dst->data[15 - i] = src->data[i]; } -static inline quint16 bt_get_le16(const void *ptr) -{ - return bt_get_unaligned((const quint16 *) ptr); -} #elif __BYTE_ORDER == __BIG_ENDIAN -static inline quint16 bt_get_le16(const void *ptr) -{ - return qbswap(bt_get_unaligned((const quint16 *) ptr)); -} static inline void btoh128(const quint128 *src, quint128 *dst) { @@ -197,6 +173,20 @@ static inline void ntoh128(const quint128 *src, quint128 *dst) #error "Unknown byte order" #endif +static inline quint16 bt_get_le16(const void *ptr) +{ + return qFromLittleEndian<quint16>(reinterpret_cast<const uchar *>(ptr)); +} + +template<typename T> inline void putBtData(T src, void *dst) +{ + qToLittleEndian(src, reinterpret_cast<uchar *>(dst)); +} +template<> inline void putBtData(quint128 src, void *dst) +{ + btoh128(&src, reinterpret_cast<quint128 *>(dst)); +} + #define hton128(x, y) ntoh128(x, y) // HCI related @@ -209,12 +199,14 @@ static inline void ntoh128(const quint128 *src, quint128 *dst) #define HCI_FILTER 2 // HCI packet types +#define HCI_COMMAND_PKT 0x01 #define HCI_EVENT_PKT 0x04 #define HCI_VENDOR_PKT 0xff #define HCI_FLT_TYPE_BITS 31 #define HCI_FLT_EVENT_BITS 63 + struct sockaddr_hci { sa_family_t hci_family; unsigned short hci_dev; @@ -339,6 +331,37 @@ typedef struct { } __attribute__ ((packed)) evt_encrypt_change; #define EVT_ENCRYPT_CHANGE_SIZE 4 +#define EVT_CMD_COMPLETE 0x0E +struct evt_cmd_complete { + quint8 ncmd; + quint16 opcode; +} __attribute__ ((packed)); + +struct hci_command_hdr { + quint16 opcode; /* OCF & OGF */ + quint8 plen; +} __attribute__ ((packed)); + +enum OpCodeGroupField { + OgfLinkControl = 0x8, +}; + +enum OpCodeCommandField { + OcfLeSetAdvParams = 0x6, + OcfLeReadTxPowerLevel = 0x7, + OcfLeSetAdvData = 0x8, + OcfLeSetScanResponseData = 0x9, + OcfLeSetAdvEnable = 0xa, + OcfLeClearWhiteList = 0x10, + OcfLeAddToWhiteList = 0x11, + OcfLeConnectionUpdate = 0x13, +}; + +/* Command opcode pack/unpack */ +#define opCodePack(ogf, ocf) (quint16(((ocf) & 0x03ff)|((ogf) << 10))) +#define ogfFromOpCode(op) ((op) >> 10) +#define ocfFromOpCode(op) ((op) & 0x03ff) + QT_END_NAMESPACE #endif // BLUEZ_DATA_P_H diff --git a/src/bluetooth/bluez/hcimanager.cpp b/src/bluetooth/bluez/hcimanager.cpp index 8f5f9dd9..123a16ab 100644 --- a/src/bluetooth/bluez/hcimanager.cpp +++ b/src/bluetooth/bluez/hcimanager.cpp @@ -41,13 +41,16 @@ #include "hcimanager_p.h" #include "qbluetoothsocket_p.h" +#include "qlowenergyconnectionparameters.h" -#include <QtCore/QLoggingCategory> +#include <QtCore/qloggingcategory.h> +#include <cstring> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> +#include <sys/uio.h> #include <unistd.h> #define HCIGETCONNLIST _IOR('H', 212, int) @@ -180,6 +183,36 @@ bool HciManager::monitorEvent(HciManager::HciEvent event) return true; } +bool HciManager::sendCommand(OpCodeGroupField ogf, OpCodeCommandField ocf, const QByteArray ¶meters) +{ + qCDebug(QT_BT_BLUEZ) << "sending command; ogf:" << ogf << "ocf:" << ocf; + quint8 packetType = HCI_COMMAND_PKT; + hci_command_hdr command = { + opCodePack(ogf, ocf), + static_cast<uint8_t>(parameters.count()) + }; + static_assert(sizeof command == 3, "unexpected struct size"); + struct iovec iv[3]; + iv[0].iov_base = &packetType; + iv[0].iov_len = 1; + iv[1].iov_base = &command; + iv[1].iov_len = sizeof command; + int ivn = 2; + if (!parameters.isEmpty()) { + iv[2].iov_base = const_cast<char *>(parameters.constData()); // const_cast is safe, since iov_base will not get modified. + iv[2].iov_len = parameters.count(); + ++ivn; + } + while (writev(hciSocket, iv, ivn) < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + qCDebug(QT_BT_BLUEZ()) << "hci command failure:" << strerror(errno); + return false; + } + qCDebug(QT_BT_BLUEZ) << "command sent successfully"; + return true; +} + /* * Unsubscribe from all events */ @@ -232,6 +265,108 @@ QBluetoothAddress HciManager::addressForConnectionHandle(quint16 handle) const return QBluetoothAddress(); } +quint16 forceIntervalIntoRange(double connectionInterval) +{ + return qMin<double>(qMax<double>(7.5, connectionInterval), 4000) / 1.25; +} + +struct ConnectionUpdateData { + quint16 minInterval; + quint16 maxInterval; + quint16 slaveLatency; + quint16 timeout; +}; +ConnectionUpdateData connectionUpdateData(const QLowEnergyConnectionParameters ¶ms) +{ + ConnectionUpdateData data; + const quint16 minInterval = forceIntervalIntoRange(params.minimumInterval()); + const quint16 maxInterval = forceIntervalIntoRange(params.maximumInterval()); + data.minInterval = qToLittleEndian(minInterval); + data.maxInterval = qToLittleEndian(maxInterval); + const quint16 latency = qMax<quint16>(0, qMin<quint16>(params.latency(), 499)); + data.slaveLatency = qToLittleEndian(latency); + const quint16 timeout + = qMax<quint16>(100, qMin<quint16>(32000, params.supervisionTimeout())) / 10; + data.timeout = qToLittleEndian(timeout); + return data; +} + +bool HciManager::sendConnectionUpdateCommand(quint16 handle, + const QLowEnergyConnectionParameters ¶ms) +{ + struct CommandParams { + quint16 handle; + ConnectionUpdateData data; + quint16 minCeLength; + quint16 maxCeLength; + } commandParams; + commandParams.handle = qToLittleEndian(handle); + commandParams.data = connectionUpdateData(params); + commandParams.minCeLength = 0; + commandParams.maxCeLength = qToLittleEndian(quint16(0xffff)); + const QByteArray data = QByteArray::fromRawData(reinterpret_cast<char *>(&commandParams), + sizeof commandParams); + return sendCommand(OgfLinkControl, OcfLeConnectionUpdate, data); +} + +bool HciManager::sendConnectionParameterUpdateRequest(quint16 handle, + const QLowEnergyConnectionParameters ¶ms) +{ + ConnectionUpdateData connUpdateData = connectionUpdateData(params); + + // Vol 3, part A, 4 + struct SignalingPacket { + quint8 code; + quint8 identifier; + quint16 length; + } signalingPacket; + signalingPacket.code = 0x12; + signalingPacket.identifier = ++sigPacketIdentifier; + const quint16 sigPacketLen = sizeof connUpdateData; + signalingPacket.length = qToLittleEndian(sigPacketLen); + + struct L2CapHeader { + quint16 length; + quint16 channelId; + } l2CapHeader; + const quint16 l2CapHeaderLen = sizeof signalingPacket + sigPacketLen; + l2CapHeader.length = qToLittleEndian(l2CapHeaderLen); + l2CapHeader.channelId = qToLittleEndian(quint16(5)); + + // Vol 2, part E, 5.4.2 + struct AclData { + quint16 handle: 12; + quint16 pbFlag: 2; + quint16 bcFlag: 2; + quint16 dataLen; + } aclData; + aclData.handle = qToLittleEndian(handle); // Works because the next two values are zero. + aclData.pbFlag = 0; + aclData.bcFlag = 0; + aclData.dataLen = qToLittleEndian(quint16(sizeof l2CapHeader + l2CapHeaderLen)); + + struct iovec iv[5]; + quint8 packetType = 2; + iv[0].iov_base = &packetType; + iv[0].iov_len = 1; + iv[1].iov_base = &aclData; + iv[1].iov_len = sizeof aclData; + iv[2].iov_base = &l2CapHeader; + iv[2].iov_len = sizeof l2CapHeader; + iv[3].iov_base = &signalingPacket; + iv[3].iov_len = sizeof signalingPacket; + iv[4].iov_base = &connUpdateData; + iv[4].iov_len = sizeof connUpdateData; + while (writev(hciSocket, iv, sizeof iv / sizeof *iv) < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + qCDebug(QT_BT_BLUEZ()) << "failure writing HCI ACL packet:" << strerror(errno); + return false; + } + qCDebug(QT_BT_BLUEZ) << "Connection Update Request packet sent successfully"; + return true; +} + /*! * Process all incoming HCI events. Function cannot process anything else but events. */ @@ -281,10 +416,59 @@ void HciManager::_q_readNotify() emit encryptionChangedEvent(remoteDevice, event->status == 0); } break; + case EVT_CMD_COMPLETE: { + auto * const event = reinterpret_cast<const evt_cmd_complete *>(data); + static_assert(sizeof *event == 3, "unexpected struct size"); + + // There is always a status byte right after the generic structure. + Q_ASSERT(size > static_cast<int>(sizeof *event)); + const quint8 status = data[sizeof *event]; + const auto additionalData = QByteArray(reinterpret_cast<const char *>(data) + + sizeof *event + 1, size - sizeof *event - 1); + emit commandCompleted(event->opcode, status, additionalData); + } + break; + case LeMetaEvent: + handleLeMetaEvent(data); + break; default: break; } } +void HciManager::handleLeMetaEvent(const quint8 *data) +{ + // Spec v4.2, Vol 2, part E, 7.7.65ff + switch (*data) { + case 0x1: { + const quint16 handle = bt_get_le16(data + 2); + emit connectionComplete(handle); + break; + } + case 0x3: { + // TODO: From little endian! + struct ConnectionUpdateData { + quint8 status; + quint16 handle; + quint16 interval; + quint16 latency; + quint16 timeout; + } __attribute((packed)); + const auto * const updateData + = reinterpret_cast<const ConnectionUpdateData *>(data + 1); + if (updateData->status == 0) { + QLowEnergyConnectionParameters params; + const double interval = qFromLittleEndian(updateData->interval) * 1.25; + params.setIntervalRange(interval, interval); + params.setLatency(qFromLittleEndian(updateData->latency)); + params.setSupervisionTimeout(qFromLittleEndian(updateData->timeout) * 10); + emit connectionUpdate(qFromLittleEndian(updateData->handle), params); + } + break; + } + default: + break; + } +} QT_END_NAMESPACE diff --git a/src/bluetooth/bluez/hcimanager_p.h b/src/bluetooth/bluez/hcimanager_p.h index f90d0bfb..b2edb15b 100644 --- a/src/bluetooth/bluez/hcimanager_p.h +++ b/src/bluetooth/bluez/hcimanager_p.h @@ -59,12 +59,16 @@ QT_BEGIN_NAMESPACE +class QLowEnergyConnectionParameters; + class HciManager : public QObject { Q_OBJECT public: enum HciEvent { EncryptChangeEvent = EVT_ENCRYPT_CHANGE, + CommandCompleteEvent = EVT_CMD_COMPLETE, + LeMetaEvent = 0x3e, }; explicit HciManager(const QBluetoothAddress &deviceAdapter, QObject *parent = 0); @@ -72,21 +76,31 @@ public: bool isValid() const; bool monitorEvent(HciManager::HciEvent event); + bool sendCommand(OpCodeGroupField ogf, OpCodeCommandField ocf, const QByteArray ¶meters); + void stopEvents(); QBluetoothAddress addressForConnectionHandle(quint16 handle) const; + bool sendConnectionUpdateCommand(quint16 handle, const QLowEnergyConnectionParameters ¶ms); + bool sendConnectionParameterUpdateRequest(quint16 handle, + const QLowEnergyConnectionParameters ¶ms); signals: void encryptionChangedEvent(const QBluetoothAddress &address, bool wasSuccess); + void commandCompleted(quint16 opCode, quint8 status, const QByteArray &data); + void connectionComplete(quint16 handle); + void connectionUpdate(quint16 handle, const QLowEnergyConnectionParameters ¶meters); private slots: void _q_readNotify(); private: int hciForAddress(const QBluetoothAddress &deviceAdapter); + void handleLeMetaEvent(const quint8 *data); int hciSocket; int hciDev; + quint8 sigPacketIdentifier = 0; QSocketNotifier *notifier; QSet<HciManager::HciEvent> runningEvents; }; diff --git a/src/bluetooth/doc/src/bluetooth-index.qdoc b/src/bluetooth/doc/src/bluetooth-index.qdoc index 41b962bf..8e5f1d35 100644 --- a/src/bluetooth/doc/src/bluetooth-index.qdoc +++ b/src/bluetooth/doc/src/bluetooth-index.qdoc @@ -84,6 +84,7 @@ import statement in your \c .qml file: \li \l {pingpong}{QML Bluetooth PingPong} \li \l {chat}{QML Bluetooth Chat} \li \l {heartlistener}{Bluetooth Low Energy Heart Listener} + \li \l {heartrate-server}{Bluetooth Low Energy Heart Rate Server} \li \l {lowenergyscanner}{Bluetooth Low Energy Scanner} \endlist \li C++ diff --git a/src/bluetooth/qbluetooth.cpp b/src/bluetooth/qbluetooth.cpp index 5665999f..5e8b812a 100644 --- a/src/bluetooth/qbluetooth.cpp +++ b/src/bluetooth/qbluetooth.cpp @@ -73,6 +73,20 @@ namespace QBluetooth { Simple Pairing from Bluetooth 2.1 or greater is required. Legacy pairing is not permitted. */ + +/*! + \enum QBluetooth::AttAccessConstraint + + This enum describes the possible requirements for reading or writing an ATT attribute. + + \value AttAuthorizationRequired + The client needs authorization from the ATT server to access the attribute. + \value AttAuthenticationRequired + The client needs to be authenticated to access the attribute. + \value AttEncryptionRequired + The attribute can only be accessed if the connection is encrypted. +*/ + } /*! diff --git a/src/bluetooth/qbluetooth.h b/src/bluetooth/qbluetooth.h index 7ab491fb..82136eca 100644 --- a/src/bluetooth/qbluetooth.h +++ b/src/bluetooth/qbluetooth.h @@ -45,6 +45,10 @@ QT_BEGIN_NAMESPACE namespace QBluetooth { + +// TODO Qt 6: Merge these two enums? But note that ATT Authorization has no equivalent +// on the socket security level. + enum Security { NoSecurity = 0x00, Authorization = 0x01, @@ -55,6 +59,16 @@ enum Security { Q_DECLARE_FLAGS(SecurityFlags, Security) Q_DECLARE_OPERATORS_FOR_FLAGS(SecurityFlags) + +enum AttAccessConstraint { + AttAuthorizationRequired = 0x1, + AttAuthenticationRequired = 0x2, + AttEncryptionRequired = 0x4, +}; + +Q_DECLARE_FLAGS(AttAccessConstraints, AttAccessConstraint) +Q_DECLARE_OPERATORS_FOR_FLAGS(AttAccessConstraints) + } typedef quint16 QLowEnergyHandle; diff --git a/src/bluetooth/qbluetoothsocket_bluez.cpp b/src/bluetooth/qbluetoothsocket_bluez.cpp index 2b1bb1f1..cfcba8af 100644 --- a/src/bluetooth/qbluetoothsocket_bluez.cpp +++ b/src/bluetooth/qbluetoothsocket_bluez.cpp @@ -273,9 +273,9 @@ void QBluetoothSocketPrivate::_q_readNotify() connectWriteNotifier->setEnabled(false); errorString = qt_error_string(errsv); qCWarning(QT_BT_BLUEZ) << Q_FUNC_INFO << socket << "error:" << readFromDevice << errorString; - if(errsv == EHOSTDOWN) + if (errsv == EHOSTDOWN) q->setSocketError(QBluetoothSocket::HostNotFoundError); - else + else if (errsv != ECONNRESET) // The other side closing the connection is not an error. q->setSocketError(QBluetoothSocket::UnknownSocketError); q->disconnectFromService(); diff --git a/src/bluetooth/qbluetoothuuid.cpp b/src/bluetooth/qbluetoothuuid.cpp index d59fb59a..3d9ab504 100644 --- a/src/bluetooth/qbluetoothuuid.cpp +++ b/src/bluetooth/qbluetoothuuid.cpp @@ -1133,11 +1133,17 @@ QString QBluetoothUuid::descriptorToString(QBluetoothUuid::DescriptorType uuid) } /*! - Returns true if \a other is equal to this Bluetooth UUID, otherwise false. + Returns \c true if \a other is equal to this Bluetooth UUID, otherwise \c false. */ bool QBluetoothUuid::operator==(const QBluetoothUuid &other) const { return QUuid::operator==(other); } +/*! + \fn bool QBluetoothUuid::operator!=(const QBluetoothUuid &other) const + Returns \c true if \a other is not equal to this Bluetooth UUID, otherwise \c false. + \since 5.7 +*/ + QT_END_NAMESPACE diff --git a/src/bluetooth/qbluetoothuuid.h b/src/bluetooth/qbluetoothuuid.h index fc00b168..e44e6ce4 100644 --- a/src/bluetooth/qbluetoothuuid.h +++ b/src/bluetooth/qbluetoothuuid.h @@ -379,6 +379,7 @@ public: ~QBluetoothUuid(); bool operator==(const QBluetoothUuid &other) const; + bool operator!=(const QBluetoothUuid &other) const { return !operator==(other); } int minimumSize() const; diff --git a/src/bluetooth/qleadvertiser_bluez.cpp b/src/bluetooth/qleadvertiser_bluez.cpp new file mode 100644 index 00000000..2e2408ba --- /dev/null +++ b/src/bluetooth/qleadvertiser_bluez.cpp @@ -0,0 +1,443 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qleadvertiser_p.h" + +#include "bluez/bluez_data_p.h" +#include "bluez/hcimanager_p.h" +#include "qbluetoothsocket_p.h" + +#include <QtCore/qloggingcategory.h> + +#include <cstring> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) + +struct AdvParams { + quint16 minInterval; + quint16 maxInterval; + quint8 type; + quint8 ownAddrType; + quint8 directAddrType; + bdaddr_t directAddr; + quint8 channelMap; + quint8 filterPolicy; +} __attribute__ ((packed)); + +struct AdvData { + quint8 length; + quint8 data[31]; +}; + +struct WhiteListParams { + quint8 addrType; + bdaddr_t addr; +}; + + +template<typename T> QByteArray byteArrayFromStruct(const T &data, int maxSize = -1) +{ + return QByteArray(reinterpret_cast<const char *>(&data), maxSize != -1 ? maxSize : sizeof data); +} + +QLeAdvertiserBluez::QLeAdvertiserBluez(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData, + HciManager &hciManager, QObject *parent) + : QLeAdvertiser(params, advertisingData, scanResponseData, parent), m_hciManager(hciManager) +{ + connect(&m_hciManager, &HciManager::commandCompleted, this, + &QLeAdvertiserBluez::handleCommandCompleted); +} + +QLeAdvertiserBluez::~QLeAdvertiserBluez() +{ + disconnect(&m_hciManager, &HciManager::commandCompleted, this, + &QLeAdvertiserBluez::handleCommandCompleted); + doStopAdvertising(); +} + +void QLeAdvertiserBluez::doStartAdvertising() +{ + if (!m_hciManager.monitorEvent(HciManager::CommandCompleteEvent)) { + handleError(); + return; + } + + m_disableCommandFinished = false; + m_sendPowerLevel = advertisingData().includePowerLevel() + || scanResponseData().includePowerLevel(); + if (m_sendPowerLevel) + queueReadTxPowerLevelCommand(); + else + queueAdvertisingCommands(); + sendNextCommand(); +} + +void QLeAdvertiserBluez::doStopAdvertising() +{ + toggleAdvertising(false); +} + +void QLeAdvertiserBluez::queueCommand(OpCodeCommandField ocf, const QByteArray &data) +{ + m_pendingCommands << Command(ocf, data); +} + +void QLeAdvertiserBluez::sendNextCommand() +{ + if (m_pendingCommands.isEmpty()) { + // TODO: Unmonitor event. + return; + } + const Command &c = m_pendingCommands.first(); + if (!m_hciManager.sendCommand(OgfLinkControl, c.ocf, c.data)) { + handleError(); + return; + } +} + +void QLeAdvertiserBluez::queueAdvertisingCommands() +{ + toggleAdvertising(false); // Stop advertising first, in case it's currently active. + setWhiteList(); + setAdvertisingParams(); + setAdvertisingData(); + setScanResponseData(); + toggleAdvertising(true); +} + +void QLeAdvertiserBluez::queueReadTxPowerLevelCommand() +{ + // Spec v4.2, Vol 2, Part E, 7.8.6 + queueCommand(OcfLeReadTxPowerLevel, QByteArray()); +} + +void QLeAdvertiserBluez::toggleAdvertising(bool enable) +{ + // Spec v4.2, Vol 2, Part E, 7.8.9 + queueCommand(OcfLeSetAdvEnable, QByteArray(1, enable)); +} + +void QLeAdvertiserBluez::setAdvertisingParams() +{ + // Spec v4.2, Vol 2, Part E, 7.8.5 + AdvParams params; + static_assert(sizeof params == 15, "unexpected struct size"); + setAdvertisingInterval(params); + params.type = parameters().mode(); + params.filterPolicy = parameters().filterPolicy(); + if (params.filterPolicy != QLowEnergyAdvertisingParameters::IgnoreWhiteList + && advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityLimited) { + qCWarning(QT_BT_BLUEZ) << "limited discoverability is incompatible with " + "using a white list; disabling filtering"; + params.filterPolicy = QLowEnergyAdvertisingParameters::IgnoreWhiteList; + } + params.ownAddrType = QLowEnergyController::PublicAddress; // TODO: Make configurable. + + // TODO: For ADV_DIRECT_IND. + // params.directAddrType = xxx; + // params.direct_bdaddr = xxx; + + params.channelMap = 0x7; // All channels. + + const QByteArray paramsData = byteArrayFromStruct(params); + qCDebug(QT_BT_BLUEZ) << "advertising parameters:" << paramsData.toHex(); + queueCommand(OcfLeSetAdvParams, paramsData); +} + +static quint16 forceIntoRange(quint16 val, quint16 min, quint16 max) +{ + return qMin(qMax(val, min), max); +} + +void QLeAdvertiserBluez::setAdvertisingInterval(AdvParams ¶ms) +{ + const double multiplier = 0.625; + const quint16 minVal = parameters().minimumInterval() / multiplier; + const quint16 maxVal = parameters().maximumInterval() / multiplier; + Q_ASSERT(minVal <= maxVal); + const quint16 specMinimum = + parameters().mode() == QLowEnergyAdvertisingParameters::AdvScanInd + || parameters().mode() == QLowEnergyAdvertisingParameters::AdvNonConnInd ? 0xa0 : 0x20; + const quint16 specMaximum = 0x4000; + params.minInterval = qToLittleEndian(forceIntoRange(minVal, specMinimum, specMaximum)); + params.maxInterval = qToLittleEndian(forceIntoRange(maxVal, specMinimum, specMaximum)); + Q_ASSERT(params.minInterval <= params.maxInterval); +} + +void QLeAdvertiserBluez::setPowerLevel(AdvData &advData) +{ + if (m_sendPowerLevel) { + advData.data[advData.length++] = 2; + advData.data[advData.length++]= 0xa; + advData.data[advData.length++] = m_powerLevel; + } +} + +void QLeAdvertiserBluez::setFlags(AdvData &advData) +{ + // TODO: Discoverability flags are incompatible with ADV_DIRECT_IND + quint8 flags = 0; + if (advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityLimited) + flags |= 0x1; + else if (advertisingData().discoverability() == QLowEnergyAdvertisingData::DiscoverabilityLimited) + flags |= 0x2; + if (flags) { + advData.data[advData.length++] = 2; + advData.data[advData.length++] = 0x1; + advData.data[advData.length++] = flags; + } +} + +template<typename T> static quint8 servicesType(bool dataComplete); +template<> quint8 servicesType<quint16>(bool dataComplete) +{ + return dataComplete ? 0x3 : 0x2; +} +template<> quint8 servicesType<quint32>(bool dataComplete) +{ + return dataComplete ? 0x5 : 0x4; +} +template<> quint8 servicesType<quint128>(bool dataComplete) +{ + return dataComplete ? 0x7 : 0x6; +} + +template<typename T> static void addServicesData(AdvData &data, const QVector<T> &services) +{ + if (services.isEmpty()) + return; + const int spaceAvailable = sizeof data.data - data.length; + const int maxServices = qMin<int>((spaceAvailable - 2) / sizeof(T), services.count()); + if (maxServices <= 0) { + qCWarning(QT_BT_BLUEZ) << "services data does not fit into advertising data packet"; + return; + } + const bool dataComplete = maxServices == services.count(); + if (!dataComplete) { + qCWarning(QT_BT_BLUEZ) << "only" << maxServices << "out of" << services.count() + << "services fit into the advertising data"; + } + data.data[data.length++] = 1 + maxServices * sizeof(T); + data.data[data.length++] = servicesType<T>(dataComplete); + for (int i = 0; i < maxServices; ++i) { + putBtData(services.at(i), data.data + data.length); + data.length += sizeof(T); + } +} + +void QLeAdvertiserBluez::setServicesData(const QLowEnergyAdvertisingData &src, AdvData &dest) +{ + QVector<quint16> services16; + QVector<quint32> services32; + QVector<quint128> services128; + foreach (const QBluetoothUuid &service, src.services()) { + bool ok; + const quint16 service16 = service.toUInt16(&ok); + if (ok) { + services16 << service16; + continue; + } + const quint32 service32 = service.toUInt32(&ok); + if (ok) { + services32 << service32; + continue; + } + services128 << service.toUInt128(); + } + addServicesData(dest, services16); + addServicesData(dest, services32); + addServicesData(dest, services128); +} + +void QLeAdvertiserBluez::setManufacturerData(const QLowEnergyAdvertisingData &src, AdvData &dest) +{ + if (src.manufacturerId() == QLowEnergyAdvertisingData::invalidManufacturerId()) + return; + if (dest.length >= sizeof dest.data - 1 - 1 - 2 - src.manufacturerData().count()) { + qCWarning(QT_BT_BLUEZ) << "manufacturer data does not fit into advertising data packet"; + return; + } + + dest.data[dest.length++] = src.manufacturerData().count() + 1 + 2; + dest.data[dest.length++] = 0xff; + putBtData(src.manufacturerId(), dest.data + dest.length); + dest.length += sizeof(quint16); + std::memcpy(dest.data + dest.length, src.manufacturerData(), src.manufacturerData().count()); + dest.length += src.manufacturerData().count(); +} + +void QLeAdvertiserBluez::setLocalNameData(const QLowEnergyAdvertisingData &src, AdvData &dest) +{ + if (src.localName().isEmpty()) + return; + if (dest.length >= sizeof dest.data - 3) { + qCWarning(QT_BT_BLUEZ) << "local name does not fit into advertising data"; + return; + } + + const QByteArray localNameUtf8 = src.localName().toUtf8(); + const int fullSize = localNameUtf8.count() + 1 + 1; + const int size = qMin<int>(fullSize, sizeof dest.data - dest.length); + const bool isComplete = size == fullSize; + dest.data[dest.length++] = size - 1; + const int dataType = isComplete ? 0x9 : 0x8; + dest.data[dest.length++] = dataType; + std::memcpy(dest.data + dest.length, localNameUtf8, size - 2); + dest.length += size - 2; +} + +void QLeAdvertiserBluez::setData(bool isScanResponseData) +{ + // Spec v4.2, Vol 3, Part C, 11 and Supplement, Part 1 + AdvData theData; + static_assert(sizeof theData == 32, "unexpected struct size"); + theData.length = 0; + + const QLowEnergyAdvertisingData &sourceData = isScanResponseData + ? scanResponseData() : advertisingData(); + + if (!sourceData.rawData().isEmpty()) { + theData.length = qMin<int>(sizeof theData.data, sourceData.rawData().count()); + std::memcpy(theData.data, sourceData.rawData().constData(), theData.length); + } else { + if (sourceData.includePowerLevel()) + setPowerLevel(theData); + if (!isScanResponseData) + setFlags(theData); + + // Insert new constant-length data here. + + setLocalNameData(sourceData, theData); + setServicesData(sourceData, theData); + setManufacturerData(sourceData, theData); + } + + const QByteArray dataToSend = byteArrayFromStruct(theData, 1 + theData.length); + if (!isScanResponseData) { + qCDebug(QT_BT_BLUEZ) << "advertising data:" << dataToSend.toHex(); + queueCommand(OcfLeSetAdvData, dataToSend); + } else if ((parameters().mode() == QLowEnergyAdvertisingParameters::AdvScanInd + || parameters().mode() == QLowEnergyAdvertisingParameters::AdvInd) + && theData.length > 0) { + qCDebug(QT_BT_BLUEZ) << "scan response data:" << dataToSend.toHex(); + queueCommand(OcfLeSetScanResponseData, dataToSend); + } +} + +void QLeAdvertiserBluez::setAdvertisingData() +{ + // Spec v4.2, Vol 2, Part E, 7.8.7 + setData(false); +} + +void QLeAdvertiserBluez::setScanResponseData() +{ + // Spec v4.2, Vol 2, Part E, 7.8.8 + setData(true); +} + +void QLeAdvertiserBluez::setWhiteList() +{ + // Spec v4.2, Vol 2, Part E, 7.8.15-16 + if (parameters().filterPolicy() == QLowEnergyAdvertisingParameters::IgnoreWhiteList) + return; + queueCommand(OcfLeClearWhiteList, QByteArray()); + foreach (const auto &addressInfo, parameters().whiteList()) { + WhiteListParams commandParam; + static_assert(sizeof commandParam == 7, "unexpected struct size"); + commandParam.addrType = addressInfo.type; + convertAddress(addressInfo.address.toUInt64(), commandParam.addr.b); + queueCommand(OcfLeAddToWhiteList, byteArrayFromStruct(commandParam)); + } +} + +void QLeAdvertiserBluez::handleCommandCompleted(quint16 opCode, quint8 status, + const QByteArray &data) +{ + if (m_pendingCommands.isEmpty()) + return; + const quint16 ocf = ocfFromOpCode(opCode); + if (m_pendingCommands.first().ocf != ocf) + return; // Not one of our commands. + m_pendingCommands.takeFirst(); + if (status != 0) { + qCDebug(QT_BT_BLUEZ) << "command" << ocf << "failed with status" << status; + if (ocf == OcfLeSetAdvEnable && !m_disableCommandFinished && status == 0xc) { + qCDebug(QT_BT_BLUEZ) << "initial advertising disable failed, ignoring"; + m_disableCommandFinished = true; + sendNextCommand(); + return; + } + if (ocf == OcfLeReadTxPowerLevel) { + qCDebug(QT_BT_BLUEZ) << "reading power level failed, leaving it out of the " + "advertising data"; + m_sendPowerLevel = false; + } else { + handleError(); + return; + } + } else { + qCDebug(QT_BT_BLUEZ) << "command" << ocf << "executed successfully"; + } + + switch (ocf) { + case OcfLeReadTxPowerLevel: + if (m_sendPowerLevel) { + m_powerLevel = data.at(0); + qCDebug(QT_BT_BLUEZ) << "TX power level is" << m_powerLevel; + } + queueAdvertisingCommands(); + break; + case OcfLeSetAdvEnable: + if (!m_disableCommandFinished) + m_disableCommandFinished = true; + break; + default: + break; + } + + sendNextCommand(); +} + +void QLeAdvertiserBluez::handleError() +{ + m_pendingCommands.clear(); + // TODO: Unmonitor event + emit errorOccurred(); +} + +QT_END_NAMESPACE diff --git a/src/bluetooth/qleadvertiser_p.h b/src/bluetooth/qleadvertiser_p.h new file mode 100644 index 00000000..ce0c74c7 --- /dev/null +++ b/src/bluetooth/qleadvertiser_p.h @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLEADVERTISER_P_H +#define QLEADVERTISER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlowenergyadvertisingdata.h" +#include "qlowenergyadvertisingparameters.h" + +#ifdef QT_BLUEZ_BLUETOOTH +#include "bluez/bluez_data_p.h" +#endif + +#include <QtCore/qobject.h> +#include <QtCore/qvector.h> + +QT_BEGIN_NAMESPACE + +class QLeAdvertiser : public QObject +{ + Q_OBJECT +public: + void startAdvertising() { doStartAdvertising(); } + void stopAdvertising() { doStopAdvertising(); } + +signals: + void errorOccurred(); + +protected: + QLeAdvertiser(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advData, + const QLowEnergyAdvertisingData &responseData, QObject *parent) + : QObject(parent), m_params(params), m_advData(advData), m_responseData(responseData) {} + virtual ~QLeAdvertiser() { } + + const QLowEnergyAdvertisingParameters ¶meters() const { return m_params; } + const QLowEnergyAdvertisingData &advertisingData() const { return m_advData; } + const QLowEnergyAdvertisingData &scanResponseData() const { return m_responseData; } + +private: + virtual void doStartAdvertising() = 0; + virtual void doStopAdvertising() = 0; + + const QLowEnergyAdvertisingParameters m_params; + const QLowEnergyAdvertisingData m_advData; + const QLowEnergyAdvertisingData m_responseData; +}; + + +#ifdef QT_BLUEZ_BLUETOOTH +struct AdvData; +struct AdvParams; +class HciManager; + +class QLeAdvertiserBluez : public QLeAdvertiser +{ +public: + QLeAdvertiserBluez(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData, HciManager &hciManager, + QObject *parent = nullptr); + ~QLeAdvertiserBluez(); + +private: + void doStartAdvertising() override; + void doStopAdvertising() override; + + void setPowerLevel(AdvData &advData); + void setFlags(AdvData &advData); + void setServicesData(const QLowEnergyAdvertisingData &src, AdvData &dest); + void setManufacturerData(const QLowEnergyAdvertisingData &src, AdvData &dest); + void setLocalNameData(const QLowEnergyAdvertisingData &src, AdvData &dest); + + void queueCommand(OpCodeCommandField ocf, const QByteArray &advertisingData); + void sendNextCommand(); + void queueAdvertisingCommands(); + void queueReadTxPowerLevelCommand(); + void toggleAdvertising(bool enable); + void setAdvertisingParams(); + void setAdvertisingInterval(AdvParams ¶ms); + void setData(bool isScanResponseData); + void setAdvertisingData(); + void setScanResponseData(); + void setWhiteList(); + + void handleCommandCompleted(quint16 opCode, quint8 status, const QByteArray &advertisingData); + void handleError(); + + HciManager &m_hciManager; + + struct Command { + Command() {} + Command(OpCodeCommandField ocf, const QByteArray &data) : ocf(ocf), data(data) { } + OpCodeCommandField ocf; + QByteArray data; + }; + QVector<Command> m_pendingCommands; + + quint8 m_powerLevel; + bool m_sendPowerLevel; + bool m_disableCommandFinished; +}; +#endif // QT_BLUEZ_BLUETOOTH + +QT_END_NAMESPACE + +#endif // Include guard. diff --git a/src/bluetooth/qlowenergyadvertisingdata.cpp b/src/bluetooth/qlowenergyadvertisingdata.cpp new file mode 100644 index 00000000..e68a1b35 --- /dev/null +++ b/src/bluetooth/qlowenergyadvertisingdata.cpp @@ -0,0 +1,280 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergyadvertisingdata.h" + +#include <cstring> + +QT_BEGIN_NAMESPACE + +class QLowEnergyAdvertisingDataPrivate : public QSharedData +{ +public: + QLowEnergyAdvertisingDataPrivate() + : manufacturerId(QLowEnergyAdvertisingData::invalidManufacturerId()) + , discoverability(QLowEnergyAdvertisingData::DiscoverabilityNone) + , includePowerLevel(false) + { + } + + QString localName; + QByteArray manufacturerData; + QByteArray rawData; + QList<QBluetoothUuid> services; + quint16 manufacturerId; + QLowEnergyAdvertisingData::Discoverability discoverability; + bool includePowerLevel; +}; + +/*! + \since 5.7 + \class QLowEnergyAdvertisingData + \brief The QLowEnergyAdvertisingData class represents the data to be broadcast during + Bluetooth Low Energy advertising. + \inmodule QtBluetooth + \ingroup shared + + This data can include the device name, GATT services offered by the device, and so on. + The data set via this class will be used when advertising is started by calling + \l QLowEnergyController::startAdvertising(). Objects of this class can represent an + Advertising Data packet or a Scan Response packet. + \note The actual data packets sent over the advertising channel cannot contain more than 31 + bytes. If the variable-length data set via this class exceeds that limit, it will + be left out of the packet or truncated, depending on the type. + + \sa QLowEnergyAdvertisingParameters + \sa QLowEnergyController::startAdvertising() +*/ + +/*! + \enum QLowEnergyAdvertisingData::Discoverability + + The discoverability of the advertising device as defined by the Generic Access Profile. + + \value DiscoverabilityNone + The advertising device does not wish to be discoverable by scanning devices. + \value DiscoverabilityLimited + The advertising device wishes to be discoverable with a high priority. Note that this mode + is not compatible with using a white list. The value of + \l QLowEnergyAdvertisingParameters::filterPolicy() is always assumed to be + \l QLowEnergyAdvertisingParameters::IgnoreWhiteList when limited discoverability + is used. + \value DiscoverabilityGeneral + The advertising device wishes to be discoverable by scanning devices. + */ + +/*! + Creates a new object of this class. All values are initialized to their defaults + according to the Bluetooth Low Energy specification. + */ +QLowEnergyAdvertisingData::QLowEnergyAdvertisingData() : d(new QLowEnergyAdvertisingDataPrivate) +{ +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyAdvertisingData::QLowEnergyAdvertisingData(const QLowEnergyAdvertisingData &other) + : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyAdvertisingData::~QLowEnergyAdvertisingData() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyAdvertisingData &QLowEnergyAdvertisingData::operator=(const QLowEnergyAdvertisingData &other) +{ + d = other.d; + return *this; +} + +/*! + Specifies that \a name should be broadcast as the name of the device. If the full name does not + fit into the advertising data packet, an abbreviated name is sent, as described by the + Bluetooth Low Energy specification. + */ +void QLowEnergyAdvertisingData::setLocalName(const QString &name) +{ + d->localName = name; +} + +/*! + Returns the name of the local device that is to be advertised. + */ +QString QLowEnergyAdvertisingData::localName() const +{ + return d->localName; +} + +/*! + Sets the manufacturer id and data. The \a id parameter is a company identifier as assigned + by the Bluetooth SIG. The \a data parameter is an arbitrary value. + */ +void QLowEnergyAdvertisingData::setManufacturerData(quint16 id, const QByteArray &data) +{ + d->manufacturerId = id; + d->manufacturerData = data; +} + +/*! + Returns the manufacturer id. + The default is \l QLowEnergyAdvertisingData::invalidManufacturerId(), which means + the data will not be advertised. + */ +quint16 QLowEnergyAdvertisingData::manufacturerId() const +{ + return d->manufacturerId; +} + +/*! + Returns the manufacturer data. The default is an empty byte array. + */ +QByteArray QLowEnergyAdvertisingData::manufacturerData() const +{ + return d->manufacturerData; +} + +/*! + Specifies whether to include the device's transmit power level in the advertising data. If + \a doInclude is \c true, the data will be included, otherwise it will not. + */ +void QLowEnergyAdvertisingData::setIncludePowerLevel(bool doInclude) +{ + d->includePowerLevel = doInclude; +} + +/*! + Returns whether to include the device's transmit power level in the advertising data. + The default is \c false. + */ +bool QLowEnergyAdvertisingData::includePowerLevel() const +{ + return d->includePowerLevel; +} + +/*! + Sets the discoverability type of the advertising device to \a mode. + \note Discoverability information can only appear in an actual advertising data packet. If + this object acts as scan response data, a call to this function will have no effect + on the scan response sent. + */ +void QLowEnergyAdvertisingData::setDiscoverability(QLowEnergyAdvertisingData::Discoverability mode) +{ + d->discoverability = mode; +} + +/*! + Returns the discoverability mode of the advertising device. + The default is \l DiscoverabilityNone. + */ +QLowEnergyAdvertisingData::Discoverability QLowEnergyAdvertisingData::discoverability() const +{ + return d->discoverability; +} + +/*! + Specifies that the service UUIDs in \a services should be advertised. + If the entire list does not fit into the packet, an incomplete list is sent as specified + by the Bluetooth Low Energy specification. + */ +void QLowEnergyAdvertisingData::setServices(const QList<QBluetoothUuid> &services) +{ + d->services = services; +} + +/*! + Returns the list of service UUIDs to be advertised. + By default, this list is empty. + */ +QList<QBluetoothUuid> QLowEnergyAdvertisingData::services() const +{ + return d->services; +} + +/*! + Sets the data to be advertised to \a data. If the value is not an empty byte array, it will + be sent as-is as the advertising data and all other data in this object will be ignored. + This can be used to send non-standard data. + \note If \a data is longer than 31 bytes, it will be truncated. It is the caller's responsibility + to ensure that \a data is well-formed. + */ +void QLowEnergyAdvertisingData::setRawData(const QByteArray &data) +{ + d->rawData = data; +} + +/*! + Returns the user-supplied raw data to be advertised. The default is an empty byte array. + */ +QByteArray QLowEnergyAdvertisingData::rawData() const +{ + return d->rawData; +} + +/*! + \fn void QLowEnergyAdvertisingData::swap(QLowEnergyAdvertisingData &other) + Swaps this object with \a other. + */ + +/*! + Returns \c true if \a data1 and \a data2 are equal with respect to their public state, + otherwise returns \c false. + */ +bool operator==(const QLowEnergyAdvertisingData &data1, const QLowEnergyAdvertisingData &data2) +{ + if (data1.d == data2.d) + return true; + return data1.discoverability() == data2.discoverability() + && data1.includePowerLevel() == data2.includePowerLevel() + && data1.localName() == data2.localName() + && data1.manufacturerData() == data2.manufacturerData() + && data1.manufacturerId() == data2.manufacturerId() + && data1.services() == data2.services() + && data1.rawData() == data2.rawData(); +} + +/*! + \fn bool operator!=(const QLowEnergyAdvertisingData &data1, + const QLowEnergyAdvertisingData &data2) + Returns \c true if \a data1 and \a data2 are not equal with respect to their public state, + otherwise returns \c false. + */ + +/*! + \fn static quint16 QLowEnergyAdvertisingData::invalidManufacturerId(); + Returns an invalid manufacturer id. If this value is set as the manufacturer id + (which it is by default), no manufacturer data will be present in the advertising data. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergyadvertisingdata.h b/src/bluetooth/qlowenergyadvertisingdata.h new file mode 100644 index 00000000..314df3f0 --- /dev/null +++ b/src/bluetooth/qlowenergyadvertisingdata.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOWENERGYADVERTISINGDATA_H +#define QLOWENERGYADVERTISINGDATA_H + +#include <QtBluetooth/qbluetoothglobal.h> +#include <QtBluetooth/qbluetoothuuid.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyAdvertisingDataPrivate; + +class Q_BLUETOOTH_EXPORT QLowEnergyAdvertisingData +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyAdvertisingData &data1, + const QLowEnergyAdvertisingData &data2); +public: + QLowEnergyAdvertisingData(); + QLowEnergyAdvertisingData(const QLowEnergyAdvertisingData &other); + ~QLowEnergyAdvertisingData(); + + QLowEnergyAdvertisingData &operator=(const QLowEnergyAdvertisingData &other); + + void setLocalName(const QString &name); + QString localName() const; + + static quint16 invalidManufacturerId() { return 0xffff; } + void setManufacturerData(quint16 id, const QByteArray &data); + quint16 manufacturerId() const; + QByteArray manufacturerData() const; + + void setIncludePowerLevel(bool doInclude); + bool includePowerLevel() const; + + enum Discoverability { + DiscoverabilityNone, DiscoverabilityLimited, DiscoverabilityGeneral + }; + void setDiscoverability(Discoverability mode); + Discoverability discoverability() const; + + void setServices(const QList<QBluetoothUuid> &services); + QList<QBluetoothUuid> services() const; + + // TODO: BR/EDR capability flag? + + void setRawData(const QByteArray &data); + QByteArray rawData() const; + + void swap(QLowEnergyAdvertisingData &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + +private: + QSharedDataPointer<QLowEnergyAdvertisingDataPrivate> d; +}; + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyAdvertisingData &data1, + const QLowEnergyAdvertisingData &data2); +inline bool operator!=(const QLowEnergyAdvertisingData &data1, + const QLowEnergyAdvertisingData &data2) +{ + return !(data1 == data2); +} + +Q_DECLARE_SHARED(QLowEnergyAdvertisingData) + +QT_END_NAMESPACE + +#endif // Include guard diff --git a/src/bluetooth/qlowenergyadvertisingparameters.cpp b/src/bluetooth/qlowenergyadvertisingparameters.cpp new file mode 100644 index 00000000..bf1e7083 --- /dev/null +++ b/src/bluetooth/qlowenergyadvertisingparameters.cpp @@ -0,0 +1,259 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergyadvertisingparameters.h" + +QT_BEGIN_NAMESPACE + +class QLowEnergyAdvertisingParametersPrivate : public QSharedData +{ +public: + QLowEnergyAdvertisingParametersPrivate() + : filterPolicy(QLowEnergyAdvertisingParameters::IgnoreWhiteList) + , mode(QLowEnergyAdvertisingParameters::AdvInd) + , minInterval(1280) + , maxInterval(1280) + { + } + + QList<QLowEnergyAdvertisingParameters::AddressInfo> whiteList; + QLowEnergyAdvertisingParameters::FilterPolicy filterPolicy; + QLowEnergyAdvertisingParameters::Mode mode; + int minInterval; + int maxInterval; +}; + +/*! + \since 5.7 + \class QLowEnergyAdvertisingParameters + \brief The QLowEnergyAdvertisingParameters class represents the parameters used for + Bluetooth Low Energy advertising. + \inmodule QtBluetooth + \ingroup shared + + When running the advertising procedure, a number of parameters can be configured, such as + how fast to advertise or which clients, if any, can connect to the advertising device. + These parameters are set via this class, and their values will be used when advertising + is started by calling \l QLowEnergyController::startAdvertising(). + + \sa QLowEnergyAdvertisingData + \sa QLowEnergyController::startAdvertising() +*/ + +/*! + \enum QLowEnergyAdvertisingParameters::Mode + + Specifies in which way to advertise. + \value AdvInd + For non-directed, connectable advertising. Advertising is not directed to + one specific device and a device seeing the advertisement can connect to the + advertising device or send scan requests. + \value AdvScanInd + For non-directed, scannable advertising. Advertising is not directed to + one specific device and a device seeing the advertisement can send a scan + request to the advertising device, but cannot connect to it. + \value AdvNonConnInd + For non-directed, non-connectable advertising. Advertising is not directed to + one specific device. A device seeing the advertisement cannot connect to the + advertising device, nor can it send a scan request. This mode thus implies + pure broadcasting. +*/ + +/*! + \enum QLowEnergyAdvertisingParameters::FilterPolicy + + Specifies the semantics of the white list. + \value IgnoreWhiteList + The value of the white list is ignored, that is, no filtering takes place for + either scan or connection requests when using undirected advertising. + \value UseWhiteListForScanning + The white list is used when handling scan requests, but is ignored for connection + requests. + \value UseWhiteListForConnecting + The white list is used when handling connection requests, but is ignored for scan + requests. + \value UseWhiteListForScanningAndConnecting + The white list is used for both connection and scan requests. + + \sa QLowEnergyAdvertisingParameters::whiteList() +*/ + +/*! + \struct QLowEnergyAdvertisingParameters::AddressInfo + + Objects of this type form the elements of a white list. + \sa QLowEnergyAdvertisingParameters::whiteList() +*/ + +/*! + \variable QLowEnergyAdvertisingParameters::AddressInfo::address + The Bluetooth address of a remote address. +*/ + +/*! + \variable QLowEnergyAdvertisingParameters::AddressInfo::type + The type of the address (public or private). +*/ + + +/*! + Constructs a new object of this class. All values are initialized to their defaults + according to the Bluetooth Low Energy specification. + */ +QLowEnergyAdvertisingParameters::QLowEnergyAdvertisingParameters() + : d(new QLowEnergyAdvertisingParametersPrivate) +{ +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyAdvertisingParameters::QLowEnergyAdvertisingParameters(const QLowEnergyAdvertisingParameters &other) + : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyAdvertisingParameters::~QLowEnergyAdvertisingParameters() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyAdvertisingParameters &QLowEnergyAdvertisingParameters::operator=(const QLowEnergyAdvertisingParameters &other) +{ + d = other.d; + return *this; +} + +/*! Sets the advertising mode to \a mode. */ +void QLowEnergyAdvertisingParameters::setMode(QLowEnergyAdvertisingParameters::Mode mode) +{ + d->mode = mode; +} + +/*! + Returns the advertising mode. The default is \l QLowEnergyAdvertisingParameters::AdvInd. + */ +QLowEnergyAdvertisingParameters::Mode QLowEnergyAdvertisingParameters::mode() const +{ + return d->mode; +} + +/*! + Sets the white list that is potentially used for filtering scan and connection requests. + The \a whiteList parameter is the list of addresses to use for filtering, and \a policy + specifies how exactly to use \a whiteList. + */ +void QLowEnergyAdvertisingParameters::setWhiteList(const QList<AddressInfo> &whiteList, + FilterPolicy policy) +{ + d->whiteList = whiteList; + d->filterPolicy = policy; +} + +/*! + Returns the white list used for filtering scan and connection requests. + By default, this list is empty. + */ +QList<QLowEnergyAdvertisingParameters::AddressInfo> QLowEnergyAdvertisingParameters::whiteList() const +{ + return d->whiteList; +} + +/*! + Returns the filter policy that determines how the white list is used. The default + is \l QLowEnergyAdvertisingParameters::IgnoreWhiteList. + */ +QLowEnergyAdvertisingParameters::FilterPolicy QLowEnergyAdvertisingParameters::filterPolicy() const +{ + return d->filterPolicy; +} + +/*! + Sets the advertising interval. This is a range that gives the controller an upper and a lower + bound for how often to send the advertising data. Both \a minimum and \a maximum are given + in milliseconds. + If \a maximum is smaller than \a minimum, it will be set to the value of \a minimum. + \note There are limits for the minimum and maximum interval; the exact values depend on + the mode. If they are exceeded, the lowest or highest possible value will be used, + respectively. + */ +void QLowEnergyAdvertisingParameters::setInterval(quint16 minimum, quint16 maximum) +{ + d->minInterval = minimum; + d->maxInterval = qMax(minimum, maximum); +} + +/*! + Returns the minimum advertising interval in milliseconds. The default is 1280. + */ +int QLowEnergyAdvertisingParameters::minimumInterval() const +{ + return d->minInterval; +} + +/*! + Returns the maximum advertising interval in milliseconds. The default is 1280. + */ +int QLowEnergyAdvertisingParameters::maximumInterval() const +{ + return d->maxInterval; +} + +/*! + \fn void QLowEnergyAdvertisingParameters::swap(QLowEnergyAdvertisingParameters &other) + Swaps this object with \a other. + */ + +/*! + Returns \a true if \a p1 and \a p2 are equal with respect to their public state, + otherwise returns false. + */ +bool operator==(const QLowEnergyAdvertisingParameters &p1, + const QLowEnergyAdvertisingParameters &p2) +{ + if (p1.d == p2.d) + return true; + return p1.filterPolicy() == p2.filterPolicy() + && p1.minimumInterval() == p2.minimumInterval() + && p1.maximumInterval() == p2.maximumInterval() + && p1.mode() == p2.mode() + && p1.whiteList() == p2.whiteList(); +} + +/*! + \fn bool operator!=(const QLowEnergyAdvertisingParameters &p1, + const QLowEnergyAdvertisingParameters &p2) + Returns \a true if \a p1 and \a p2 are not equal with respect to their public state, + otherwise returns false. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergyadvertisingparameters.h b/src/bluetooth/qlowenergyadvertisingparameters.h new file mode 100644 index 00000000..8d98a10b --- /dev/null +++ b/src/bluetooth/qlowenergyadvertisingparameters.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOWENERGYADVERTISINGPARAMETERS_H +#define QLOWENERGYADVERTISINGPARAMETERS_H + +#include <QtBluetooth/qbluetoothglobal.h> +#include <QtBluetooth/qbluetoothaddress.h> +#include <QtBluetooth/qlowenergycontroller.h> +#include <QtCore/qlist.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyAdvertisingParametersPrivate; + +class Q_BLUETOOTH_EXPORT QLowEnergyAdvertisingParameters +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyAdvertisingParameters &p1, + const QLowEnergyAdvertisingParameters &p2); +public: + QLowEnergyAdvertisingParameters(); + QLowEnergyAdvertisingParameters(const QLowEnergyAdvertisingParameters &other); + ~QLowEnergyAdvertisingParameters(); + + QLowEnergyAdvertisingParameters &operator=(const QLowEnergyAdvertisingParameters &other); + + enum Mode { AdvInd = 0x0, AdvScanInd = 0x2, AdvNonConnInd = 0x3 }; + void setMode(Mode mode); + Mode mode() const; + + struct AddressInfo { + AddressInfo(const QBluetoothAddress &addr, QLowEnergyController::RemoteAddressType t) + : address(addr), type(t) {} + AddressInfo() {} + + QBluetoothAddress address; + QLowEnergyController::RemoteAddressType type; + }; + enum FilterPolicy { + IgnoreWhiteList = 0x00, + UseWhiteListForScanning = 0x01, + UseWhiteListForConnecting = 0x02, + UseWhiteListForScanningAndConnecting = 0x03, + }; + void setWhiteList(const QList<AddressInfo> &whiteList, FilterPolicy policy); + QList<AddressInfo> whiteList() const; + FilterPolicy filterPolicy() const; + + void setInterval(quint16 minimum, quint16 maximum); + int minimumInterval() const; + int maximumInterval() const; + + // TODO: own address type + // TODO: For ADV_DIRECT_IND: peer address + peer address type + + void swap(QLowEnergyAdvertisingParameters &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + +private: + QSharedDataPointer<QLowEnergyAdvertisingParametersPrivate> d; +}; + +inline bool operator==(const QLowEnergyAdvertisingParameters::AddressInfo &ai1, + const QLowEnergyAdvertisingParameters::AddressInfo &ai2) +{ + return ai1.address == ai2.address && ai1.type == ai2.type; +} + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyAdvertisingParameters &p1, + const QLowEnergyAdvertisingParameters &p2); +inline bool operator!=(const QLowEnergyAdvertisingParameters &p1, + const QLowEnergyAdvertisingParameters &p2) +{ + return !(p1 == p2); +} + +Q_DECLARE_SHARED(QLowEnergyAdvertisingParameters) + +QT_END_NAMESPACE + +#endif // Include guard diff --git a/src/bluetooth/qlowenergycharacteristicdata.cpp b/src/bluetooth/qlowenergycharacteristicdata.cpp new file mode 100644 index 00000000..a200f1c0 --- /dev/null +++ b/src/bluetooth/qlowenergycharacteristicdata.cpp @@ -0,0 +1,273 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergycharacteristicdata.h" + +#include "qlowenergydescriptordata.h" + +#include <QtCore/qbytearray.h> +#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) + , minimumValueLength(0) + , maximumValueLength(INT_MAX) + {} + + QBluetoothUuid uuid; + QLowEnergyCharacteristic::PropertyTypes properties; + QList<QLowEnergyDescriptorData> descriptors; + QByteArray value; + QBluetooth::AttAccessConstraints readConstraints; + QBluetooth::AttAccessConstraints writeConstraints; + int minimumValueLength; + int maximumValueLength; +}; + +/*! + \since 5.7 + \class QLowEnergyCharacteristicData + \brief The QLowEnergyCharacteristicData class is used to set up GATT service data. + \inmodule QtBluetooth + \ingroup shared + + An Object of this class provides a characteristic to be added to a + \l QLowEnergyServiceData object via \l QLowEnergyServiceData::addCharacteristic(). + + \sa QLowEnergyServiceData + \sa QLowEnergyController::addService +*/ + +/*! Creates a new invalid object of this class. */ +QLowEnergyCharacteristicData::QLowEnergyCharacteristicData() + : d(new QLowEnergyCharacteristicDataPrivate) +{ +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyCharacteristicData::QLowEnergyCharacteristicData(const QLowEnergyCharacteristicData &other) + : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyCharacteristicData::~QLowEnergyCharacteristicData() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyCharacteristicData &QLowEnergyCharacteristicData::operator=(const QLowEnergyCharacteristicData &other) +{ + d = other.d; + return *this; +} + +/*! Returns the UUID of this characteristic. */ +QBluetoothUuid QLowEnergyCharacteristicData::uuid() const +{ + return d->uuid; +} + +/*! Sets the UUID of this characteristic to \a uuid. */ +void QLowEnergyCharacteristicData::setUuid(const QBluetoothUuid &uuid) +{ + d->uuid = uuid; +} + +/*! Returns the value of this characteristic. */ +QByteArray QLowEnergyCharacteristicData::value() const +{ + return d->value; +} + +/*! Sets the value of this characteristic to \a value. */ +void QLowEnergyCharacteristicData::setValue(const QByteArray &value) +{ + d->value = value; +} + +/*! Returns the properties of this characteristic. */ +QLowEnergyCharacteristic::PropertyTypes QLowEnergyCharacteristicData::properties() const +{ + return d->properties; +} + +/*! Sets the properties of this characteristic to \a properties. */ +void QLowEnergyCharacteristicData::setProperties(QLowEnergyCharacteristic::PropertyTypes properties) +{ + d->properties = properties; +} + +/*! Returns the descriptors of this characteristic. */ +QList<QLowEnergyDescriptorData> QLowEnergyCharacteristicData::descriptors() const +{ + return d->descriptors; +} + +/*! + Sets the descriptors of this characteristic to \a descriptors. Only valid descriptors + are considered. + \sa addDescriptor() + */ +void QLowEnergyCharacteristicData::setDescriptors(const QList<QLowEnergyDescriptorData> &descriptors) +{ + foreach (const QLowEnergyDescriptorData &desc, descriptors) + addDescriptor(desc); +} + +/*! + Adds \a descriptor to the list of descriptors of this characteristic, if it is valid. + \sa setDescriptors() + */ +void QLowEnergyCharacteristicData::addDescriptor(const QLowEnergyDescriptorData &descriptor) +{ + if (descriptor.isValid()) + d->descriptors << descriptor; + else + qCWarning(QT_BT) << "not adding invalid descriptor to characteristic"; +} + +/*! + Specifies that clients need to fulfill \a constraints to read the value of this characteristic. + */ +void QLowEnergyCharacteristicData::setReadConstraints(QBluetooth::AttAccessConstraints constraints) +{ + d->readConstraints = constraints; +} + +/*! + Returns the constraints needed for a client to read the value of this characteristic. + If \l properties() does not include \l QLowEnergyCharacteristic::Read, this value is irrelevant. + By default, there are no read constraints. + */ +QBluetooth::AttAccessConstraints QLowEnergyCharacteristicData::readConstraints() const +{ + return d->readConstraints; +} + +/*! + Specifies that clients need to fulfill \a constraints to write the value of this characteristic. + */ +void QLowEnergyCharacteristicData::setWriteConstraints(QBluetooth::AttAccessConstraints constraints) +{ + d->writeConstraints = constraints; +} + +/*! + Returns the constraints needed for a client to write the value of this characteristic. + If \l properties() does not include either of \l QLowEnergyCharacteristic::Write, + \l QLowEnergyCharacteristic::WriteNoResponse and \l QLowEnergyCharacteristic::WriteSigned, + this value is irrelevant. + By default, there are no write constraints. + */ +QBluetooth::AttAccessConstraints QLowEnergyCharacteristicData::writeConstraints() const +{ + return d->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 +{ + return !uuid().isNull(); +} + +/*! + \fn void QLowEnergyCharacteristicData::swap(QLowEnergyCharacteristicData &other) + Swaps this object with \a other. + */ + +/*! + Returns \c true if \a cd1 and \a cd2 are equal with respect to their public state, + otherwise returns \c false. + */ +bool operator==(const QLowEnergyCharacteristicData &cd1, const QLowEnergyCharacteristicData &cd2) +{ + return cd1.d == cd2.d || ( + cd1.uuid() == cd2.uuid() + && cd1.properties() == cd2.properties() + && cd1.descriptors() == cd2.descriptors() + && cd1.value() == cd2.value() + && cd1.readConstraints() == cd2.readConstraints() + && cd1.writeConstraints() == cd2.writeConstraints() + && cd1.minimumValueLength() == cd2.maximumValueLength() + && cd1.maximumValueLength() == cd2.maximumValueLength()); +} + +/*! + \fn bool operator!=(const QLowEnergyCharacteristicData &cd1, + const QLowEnergyCharacteristicData &cd2) + Returns \c true if \a cd1 and \a cd2 are not equal with respect to their public state, + otherwise returns \c false. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycharacteristicdata.h b/src/bluetooth/qlowenergycharacteristicdata.h new file mode 100644 index 00000000..56493198 --- /dev/null +++ b/src/bluetooth/qlowenergycharacteristicdata.h @@ -0,0 +1,97 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#ifndef QLOWENERGYCHARACTERISTICDATA_H +#define QLOWENERGYCHARACTERISTICDATA_H + +#include <QtBluetooth/qlowenergycharacteristic.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyDescriptorData; +struct QLowEnergyCharacteristicDataPrivate; +class Q_BLUETOOTH_EXPORT QLowEnergyCharacteristicData +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyCharacteristicData &cd1, + const QLowEnergyCharacteristicData &cd2); +public: + QLowEnergyCharacteristicData(); + QLowEnergyCharacteristicData(const QLowEnergyCharacteristicData &other); + ~QLowEnergyCharacteristicData(); + + QLowEnergyCharacteristicData &operator=(const QLowEnergyCharacteristicData &other); + + QBluetoothUuid uuid() const; + void setUuid(const QBluetoothUuid &uuid); + + QByteArray value() const; + void setValue(const QByteArray &value); + + QLowEnergyCharacteristic::PropertyTypes properties() const; + void setProperties(QLowEnergyCharacteristic::PropertyTypes properties); + + QList<QLowEnergyDescriptorData> descriptors() const; + void setDescriptors(const QList<QLowEnergyDescriptorData> &descriptors); + void addDescriptor(const QLowEnergyDescriptorData &descriptor); + + void setReadConstraints(QBluetooth::AttAccessConstraints constraints); + QBluetooth::AttAccessConstraints readConstraints() const; + + 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); } + +private: + QSharedDataPointer<QLowEnergyCharacteristicDataPrivate> d; +}; + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyCharacteristicData &cd1, + const QLowEnergyCharacteristicData &cd2); +inline bool operator!=(const QLowEnergyCharacteristicData &cd1, + const QLowEnergyCharacteristicData &cd2) +{ + return !(cd1 == cd2); +} + +Q_DECLARE_SHARED(QLowEnergyCharacteristicData) + +QT_END_NAMESPACE + +#endif // Include guard. diff --git a/src/bluetooth/qlowenergyconnectionparameters.cpp b/src/bluetooth/qlowenergyconnectionparameters.cpp new file mode 100644 index 00000000..727ed141 --- /dev/null +++ b/src/bluetooth/qlowenergyconnectionparameters.cpp @@ -0,0 +1,202 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergyconnectionparameters.h" + +QT_BEGIN_NAMESPACE + +class QLowEnergyConnectionParametersPrivate : public QSharedData +{ +public: + QLowEnergyConnectionParametersPrivate() + : minInterval(7.5) + , maxInterval(4000) + , latency(0) + , timeout(32000) + { + } + + double minInterval; + double maxInterval; + int latency; + int timeout; +}; + +/*! + \since 5.7 + \class QLowEnergyConnectionParameters + \brief The QLowEnergyConnectionParameters class is used when requesting or reporting + an update of the parameters of a Bluetooth LE connection. + + The connection parameters influence how often a master and a slave device synchronize + with each other. In general, a lower connection interval and latency means faster communication, + but also higher power consumption. How these criteria should be weighed against each other + is highly dependent on the concrete use case. + \inmodule QtBluetooth + \ingroup shared + + \sa QLowEnergyController::requestConnectionUpdate + \sa QLowEnergyController::connectionUpdated +*/ + + +/*! + Constructs a new object of this class. All values are initialized to valid defaults. + */ +QLowEnergyConnectionParameters::QLowEnergyConnectionParameters() + : d(new QLowEnergyConnectionParametersPrivate) +{ +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyConnectionParameters::QLowEnergyConnectionParameters(const QLowEnergyConnectionParameters &other) + : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyConnectionParameters::~QLowEnergyConnectionParameters() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyConnectionParameters &QLowEnergyConnectionParameters::operator=(const QLowEnergyConnectionParameters &other) +{ + d = other.d; + return *this; +} + +/*! + Sets the range in which the connection interval should be. The actual value will be decided by + the controller. Both \a minimum and \a maximum are given in milliseconds. + If \a maximum is smaller than \a minimum, it will be set to the value of \a minimum. + The smallest possible connection interval is 7.5 milliseconds, the largest one is + 4000 milliseconds. + \sa minimumInterval(), maximumInterval() + */ +void QLowEnergyConnectionParameters::setIntervalRange(double minimum, double maximum) +{ + d->minInterval = minimum; + d->maxInterval = qMax(minimum, maximum); +} + +/*! + Returns the minimum connection interval in milliseconds. The default is 7.5. + \note If this object was emitted via \l QLowEnergyController::connectionUpdated(), then + this value is the same as \l maximumInterval() and refers to the actual + connection interval. + \sa setIntervalRange() + */ +double QLowEnergyConnectionParameters::minimumInterval() const +{ + return d->minInterval; +} + +/*! + Returns the maximum connection interval in milliseconds. The default is 4000. + \note If this object was emitted via \l QLowEnergyController::connectionUpdated(), then + this value is the same as \l minimumInterval() and refers to the actual + connection interval. + \sa setIntervalRange() + */ +double QLowEnergyConnectionParameters::maximumInterval() const +{ + return d->maxInterval; +} + +/*! + Sets the slave latency of the connection (that is, the number of connection events that a slave + device is allowed to ignore) to \a latency. The minimum value is 0, the maximum is 499. + \sa latency() + */ +void QLowEnergyConnectionParameters::setLatency(int latency) +{ + d->latency = latency; +} + +/*! + Returns the slave latency of the connection. + \sa setLatency() +*/ +int QLowEnergyConnectionParameters::latency() const +{ + return d->latency; +} + +/*! + Sets the link supervision timeout to \a timeout milliseconds. + There are several constraints on this value: It must be in the range [100,32000] and it must be + larger than (1 + \l latency()) * 2 * \l maximumInterval(). + \sa supervisionTimeout() + */ +void QLowEnergyConnectionParameters::setSupervisionTimeout(int timeout) +{ + d->timeout = timeout; +} + +/*! + Returns the link supervision timeout of the connection in milliseconds. + \sa setSupervisionTimeout() +*/ +int QLowEnergyConnectionParameters::supervisionTimeout() const +{ + return d->timeout; +} + +/*! + \fn void QLowEnergyConnectionParameters::swap(QLowEnergyConnectionParameters &other) + Swaps this object with \a other. + */ + +/*! + Returns \a true if \a p1 and \a p2 are equal with respect to their public state, + otherwise returns false. + */ +bool operator==(const QLowEnergyConnectionParameters &p1, const QLowEnergyConnectionParameters &p2) +{ + if (p1.d == p2.d) + return true; + return p1.minimumInterval() == p2.minimumInterval() + && p1.maximumInterval() == p2.maximumInterval() + && p1.latency() == p2.latency() + && p1.supervisionTimeout() == p2.supervisionTimeout(); +} + +/*! + \fn bool operator!=(const QLowEnergyConnectionParameters &p1, + const QLowEnergyConnectionParameters &p2) + Returns \a true if \a p1 and \a p2 are not equal with respect to their public state, + otherwise returns false. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergyconnectionparameters.h b/src/bluetooth/qlowenergyconnectionparameters.h new file mode 100644 index 00000000..a7022dd2 --- /dev/null +++ b/src/bluetooth/qlowenergyconnectionparameters.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOWENERGYCONNECTIONPARAMETERS_H +#define QLOWENERGYCONNECTIONPARAMETERS_H + +#include <QtBluetooth/qbluetoothglobal.h> +#include <QtCore/qmetatype.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyConnectionParametersPrivate; + +class Q_BLUETOOTH_EXPORT QLowEnergyConnectionParameters +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyConnectionParameters &p1, + const QLowEnergyConnectionParameters &p2); +public: + QLowEnergyConnectionParameters(); + QLowEnergyConnectionParameters(const QLowEnergyConnectionParameters &other); + ~QLowEnergyConnectionParameters(); + + QLowEnergyConnectionParameters &operator=(const QLowEnergyConnectionParameters &other); + + void setIntervalRange(double minimum, double maximum); + double minimumInterval() const; + double maximumInterval() const; + + void setLatency(int latency); + int latency() const; + + void setSupervisionTimeout(int timeout); + int supervisionTimeout() const; + + void swap(QLowEnergyConnectionParameters &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + +private: + QSharedDataPointer<QLowEnergyConnectionParametersPrivate> d; +}; + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyConnectionParameters &p1, + const QLowEnergyConnectionParameters &p2); +inline bool operator!=(const QLowEnergyConnectionParameters &p1, + const QLowEnergyConnectionParameters &p2) +{ + return !(p1 == p2); +} + +Q_DECLARE_SHARED(QLowEnergyConnectionParameters) + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QLowEnergyConnectionParameters) + +#endif // Include guard diff --git a/src/bluetooth/qlowenergycontroller.cpp b/src/bluetooth/qlowenergycontroller.cpp index 4eebc779..0cbf6582 100644 --- a/src/bluetooth/qlowenergycontroller.cpp +++ b/src/bluetooth/qlowenergycontroller.cpp @@ -40,12 +40,20 @@ #include "qlowenergycontroller.h" #include "qlowenergycontroller_p.h" +#include "qlowenergycharacteristicdata.h" +#include "qlowenergyconnectionparameters.h" +#include "qlowenergydescriptordata.h" +#include "qlowenergyservicedata.h" + #include <QtBluetooth/QBluetoothLocalDevice> +#include <QtCore/QLoggingCategory> #include <algorithm> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(QT_BT) + /*! \class QLowEnergyController \inmodule QtBluetooth @@ -55,9 +63,7 @@ QT_BEGIN_NAMESPACE \since 5.4 QLowEnergyController acts as the entry point for Bluetooth Low Energy - development. Each QLowEnergyController instance acts as placeholder - towards a remote Low Energy device enabling connection control, - service discovery and state tracking. + development. Bluetooth Low Energy defines two types of devices; the peripheral and the central. Each role performs a different task. The peripheral device @@ -68,12 +74,12 @@ QT_BEGIN_NAMESPACE the sensor is the peripheral device and the mobile phone acts as the central device. - At the moment Qt only supports the central role and therefore the remote - device can only be a device acting as a peripheral. This implies that the local - device acts within the boundaries of the central role as per the Bluetooth 4.0 - specification. + A controller in the central role is created via the \l createCentral() factory method. + Such an object essentially acts as a placeholder towards a remote Low Energy peripheral + device, enabling features such as service discovery and state tracking. - The first step is to establish a connection via \l connectToDevice(). + After having created a controller object in the central role, the first step is to establish + a connection via \l connectToDevice(). Once the connection has been established, the controller's \l state() changes to \l QLowEnergyController::ConnectedState and the \l connected() signal is emitted. It is important to mention that some platforms such as @@ -99,7 +105,18 @@ QT_BEGIN_NAMESPACE connection becomes invalid as soon as the controller disconnects from the remote Bluetooth Low Energy device. + A controller in the peripheral role is created via the \l createPeripheral() factory method. + Such an object acts as a peripheral device itself, enabling features such as advertising + services and allowing clients to get notified about changes to characteristic values. + + After having created a controller object in the peripheral role, the first step is to + populate the set of GATT services offered to client devices via calls to \l addService(). + Afterwards, one would call \l startAdvertising() to let the device broadcast some data + and, depending on the type of advertising being done, also listen for incoming connections + from GATT clients. + \sa QLowEnergyService, QLowEnergyCharacteristic, QLowEnergyDescriptor + \sa QLowEnergyAdvertisingParameters, QLowEnergyAdvertisingData */ /*! @@ -119,6 +136,8 @@ QT_BEGIN_NAMESPACE there is no local Bluetooth device. \value ConnectionError The attempt to connect to the remote device failed. This value was introduced by Qt 5.5. + \value AdvertisingError The attempt to start advertising failed. + This value was introduced by Qt 5.7. */ /*! @@ -134,6 +153,7 @@ QT_BEGIN_NAMESPACE \value DiscoveredState The controller has discovered all services offered by the remote device. \value ClosingState The controller is about to be disconnected from the remote device. + \value AdvertisingState The controller is currently advertising data. */ /*! @@ -141,26 +161,47 @@ QT_BEGIN_NAMESPACE Indicates what type of Bluetooth address the remote device uses. - \value PublicAddress The peripheral uses a public Bluetooth address. + \value PublicAddress The remote device uses a public Bluetooth address. \value RandomAddress A random address is a Bluetooth Low Energy security feature. Peripherals using such addresses may frequently change their Bluetooth address. This information is needed when trying to connect to a peripheral. */ +/*! + \enum QLowEnergyController::Role + + Indicates the role of the controller object. + + \value CentralRole + The controller acts as a client interacting with a remote device which is in the peripheral + role. The controller can initiate connections, discover services and + read and write characteristics. + \value PeripheralRole + The controller can be used to advertise services and handle incoming + connections and client requests, acting as a GATT server. A remote device connected to + the controller is in the central role. + + \sa QLowEnergyController::createCentral() + \sa QLowEnergyController::createPeripheral() + \since 5.7 + \note The peripheral role is currently only supported on Linux. + */ + /*! \fn void QLowEnergyController::connected() This signal is emitted when the controller successfully connects to the remote - Low Energy device. + Low Energy device (if the controller is in the \l CentralRole) or if a remote Low Energy + device connected to the controller (if the controller is in the \l PeripheralRole). */ /*! \fn void QLowEnergyController::disconnected() This signal is emitted when the controller disconnects from the remote - Low Energy device. + Low Energy device or vice versa. */ /*! @@ -187,6 +228,8 @@ QT_BEGIN_NAMESPACE This signal is emitted each time a new service is discovered. The \a newService parameter contains the UUID of the found service. + This signal can only be emitted if the controller is in the \c CentralRole. + \sa discoverServices(), discoveryFinished() */ @@ -197,15 +240,30 @@ QT_BEGIN_NAMESPACE The signal is not emitted if the discovery process finishes with an error. + This signal can only be emitted if the controller is in the \l CentralRole. + \sa discoverServices(), error() */ +/*! + \fn void QLowEnergyController::connectionUpdated(const QLowEnergyConnectionParameters &newParameters) + + This signal is emitted when the connection parameters change. This can happen as a result + of calling \l requestConnectionUpdate() or due to other reasons, for instance because + the other side of the connection requested new parameters. The new values can be retrieved + from \a newParameters. + + \sa requestConnectionUpdate() +*/ + + void registerQLowEnergyControllerMetaType() { static bool initDone = false; if (!initDone) { qRegisterMetaType<QLowEnergyController::ControllerState>(); qRegisterMetaType<QLowEnergyController::Error>(); + qRegisterMetaType<QLowEnergyConnectionParameters>(); initDone = true; } } @@ -230,6 +288,9 @@ void QLowEnergyControllerPrivate::setError( case QLowEnergyController::ConnectionError: errorString = QLowEnergyController::tr("Error occurred trying to connect to remote device."); break; + case QLowEnergyController::AdvertisingError: + errorString = QLowEnergyController::tr("Error occurred trying to start advertising"); + break; case QLowEnergyController::NoError: return; default: @@ -267,6 +328,10 @@ void QLowEnergyControllerPrivate::setState( return; state = newState; + if (state == QLowEnergyController::UnconnectedState + && role == QLowEnergyController::PeripheralRole) { + remoteDevice.clear(); + } emit q->stateChanged(state); } @@ -415,6 +480,7 @@ QLowEnergyController::QLowEnergyController( { Q_D(QLowEnergyController); d->q_ptr = this; + d->role = CentralRole; d->remoteDevice = remoteDevice; d->localAdapter = QBluetoothLocalDevice().address(); d->addressType = QLowEnergyController::PublicAddress; @@ -431,6 +497,7 @@ QLowEnergyController::QLowEnergyController( the connection management. \since 5.5 + \obsolete */ QLowEnergyController::QLowEnergyController( const QBluetoothDeviceInfo &remoteDeviceInfo, @@ -439,6 +506,7 @@ QLowEnergyController::QLowEnergyController( { Q_D(QLowEnergyController); d->q_ptr = this; + d->role = CentralRole; d->remoteDevice = remoteDeviceInfo.address(); d->localAdapter = QBluetoothLocalDevice().address(); d->addressType = QLowEnergyController::PublicAddress; @@ -468,11 +536,47 @@ QLowEnergyController::QLowEnergyController( { Q_D(QLowEnergyController); d->q_ptr = this; + d->role = CentralRole; d->remoteDevice = remoteDevice; d->localAdapter = localDevice; } /*! + Returns a new object of this class that is in the \l CentralRole. + The \a remoteDevice refers to the device that a connection will be established to later. + * + The controller uses the local default Bluetooth adapter for the connection management. + \sa QLowEnergyController::CentralRole + */ +QLowEnergyController *QLowEnergyController::createCentral(const QBluetoothDeviceInfo &remoteDevice, + QObject *parent) +{ + return new QLowEnergyController(remoteDevice, parent); +} + + +/*! + Returns a new object of this class that is in the \l PeripheralRole. + Typically, the next step is to call \l startAdvertising() on the returned object. + * + The controller uses the local default Bluetooth adapter for the connection management. + \sa QLowEnergyController::PeripheralRole + */ +QLowEnergyController *QLowEnergyController::createPeripheral(QObject *parent) +{ + return new QLowEnergyController(parent); +} + +QLowEnergyController::QLowEnergyController(QObject *parent) + : QObject(parent), d_ptr(new QLowEnergyControllerPrivate()) +{ + Q_D(QLowEnergyController); + d->q_ptr = this; + d->role = PeripheralRole; + d->localAdapter = QBluetoothLocalDevice().address(); +} + +/*! Destroys the QLowEnergyController instance. */ QLowEnergyController::~QLowEnergyController() @@ -498,6 +602,11 @@ QBluetoothAddress QLowEnergyController::localAddress() const /*! Returns the address of the remote Bluetooth Low Energy device. + + For a controller in the \l CentralRole, this value will always be the one passed in when + the controller object was created. For a controller in the \l PeripheralRole, this value + is the address of the currently connected client device. In particular, this address will + be invalid if the controller is not currently in the \l ConnectedState. */ QBluetoothAddress QLowEnergyController::remoteAddress() const { @@ -505,7 +614,8 @@ QBluetoothAddress QLowEnergyController::remoteAddress() const } /*! - Returns the name of the remote Bluetooth Low Energy device. + Returns the name of the remote Bluetooth Low Energy device, if the controller is in the + \l CentralRole. Otherwise the result is unspecified. \since 5.5 */ @@ -617,6 +727,10 @@ void QLowEnergyController::discoverServices() { Q_D(QLowEnergyController); + if (d->role != CentralRole) { + qCWarning(QT_BT) << "Cannot discover services in peripheral role"; + return; + } if (d->state != QLowEnergyController::ConnectedState) return; @@ -625,7 +739,8 @@ void QLowEnergyController::discoverServices() } /*! - Returns the list of services offered by the remote device. + Returns the list of services offered by the remote device, if the controller is in + the \l CentralRole. Otherwise, the result is unspecified. The list contains all primary and secondary services. @@ -678,6 +793,131 @@ QLowEnergyService *QLowEnergyController::createServiceObject( } /*! + Starts advertising the data given in \a advertisingData and \a scanResponseData, using + the parameters set in \a parameters. The controller has to be in the \l PeripheralRole. + If \a parameters indicates that the advertisement should be connectable, then this function + also starts listening for incoming client connections. + + Providing \a scanResponseData is not required, as it is not applicable for certain + configurations of \c parameters. + + If this object is currently not in the \l UnconnectedState, nothing happens. + \note Advertising will stop automatically once a client connects to the local device. + */ +void QLowEnergyController::startAdvertising(const QLowEnergyAdvertisingParameters ¶meters, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData) +{ + Q_D(QLowEnergyController); + if (role() != PeripheralRole) { + qCWarning(QT_BT) << "Cannot start advertising in central role" << state(); + return; + } + if (state() != UnconnectedState) { + qCWarning(QT_BT) << "Cannot start advertising in state" << state(); + return; + } + d->startAdvertising(parameters, advertisingData, scanResponseData); +} + +/*! + Stops advertising, if this object is currently in the advertising state. + */ +void QLowEnergyController::stopAdvertising() +{ + Q_D(QLowEnergyController); + if (state() != AdvertisingState) { + qCDebug(QT_BT) << "stopAdvertising called in state" << state(); + return; + } + d->stopAdvertising(); +} + +/*! + Constructs and returns a \l QLowEnergyService object with \a parent from \a service. + The controller must be in the \l PeripheralRole and in the \l UnconnectedState. The \a service + object must be valid. + */ +QLowEnergyService *QLowEnergyController::addService(const QLowEnergyServiceData &service, + QObject *parent) +{ + if (role() != PeripheralRole) { + qCWarning(QT_BT) << "Services can only be added in the peripheral role"; + return nullptr; + } + if (state() != UnconnectedState) { + qCWarning(QT_BT) << "Services can only be added in unconnected state"; + return nullptr; + } + if (!service.isValid()) { + qCWarning(QT_BT) << "Not adding invalid service"; + return nullptr; + } + + // Spec says services "should" be grouped by uuid length (16-bit first, then 128-bit). + // Since this is not mandatory, we ignore it here and let the caller take responsibility + // 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; + foreach (QLowEnergyService * const includedService, service.includedServices()) { + servicePrivate->includedServices << includedService->serviceUuid(); + includedService->d_ptr->type |= QLowEnergyService::IncludedService; + } + + // Spec v4.2, Vol 3, Part G, Section 3. + const QLowEnergyHandle oldLastHandle = d_ptr->lastLocalHandle; + servicePrivate->startHandle = ++d_ptr->lastLocalHandle; // Service declaration. + d_ptr->lastLocalHandle += servicePrivate->includedServices.count(); // Include declarations. + foreach (const QLowEnergyCharacteristicData &cd, service.characteristics()) { + const QLowEnergyHandle declHandle = ++d_ptr->lastLocalHandle; + QLowEnergyServicePrivate::CharData charData; + charData.valueHandle = ++d_ptr->lastLocalHandle; + charData.uuid = cd.uuid(); + charData.properties = cd.properties(); + charData.value = cd.value(); + foreach (const QLowEnergyDescriptorData &dd, cd.descriptors()) { + QLowEnergyServicePrivate::DescData descData; + descData.uuid = dd.uuid(); + descData.value = dd.value(); + charData.descriptorList.insert(++d_ptr->lastLocalHandle, descData); + } + servicePrivate->characteristicList.insert(declHandle, charData); + } + servicePrivate->endHandle = d_ptr->lastLocalHandle; + const bool handleOverflow = d_ptr->lastLocalHandle <= oldLastHandle; + if (handleOverflow) { + qCWarning(QT_BT) << "Not enough attribute handles left to create this service"; + d_ptr->lastLocalHandle = oldLastHandle; + return nullptr; + } + + d_ptr->localServices.insert(servicePrivate->uuid, servicePrivate); + d_ptr->addToGenericAttributeList(service, servicePrivate->startHandle); + return new QLowEnergyService(servicePrivate, parent); +} + +/*! + Requests the controller to update the connection according to \a parameters. + If the request is successful, the \l connectionUpdated() signal will be emitted + with the actual new parameters. + See the \l QLowEnergyConnectionParameters class for more information on connection parameters. + \note Currently, this functionality is only implemented on Linux. + */ +void QLowEnergyController::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶meters) +{ + if (state() != ConnectedState) { + qCWarning(QT_BT) << "Connection update request only possible in connected state"; + return; + } + d_ptr->requestConnectionUpdate(parameters); +} + +/*! Returns the last occurred error or \l NoError. */ QLowEnergyController::Error QLowEnergyController::error() const @@ -694,4 +934,12 @@ QString QLowEnergyController::errorString() const return d_ptr->errorString; } +/*! + Returns the role that this controller object is in. + */ +QLowEnergyController::Role QLowEnergyController::role() const +{ + return d_ptr->role; +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller.h b/src/bluetooth/qlowenergycontroller.h index 22617942..12a91a75 100644 --- a/src/bluetooth/qlowenergycontroller.h +++ b/src/bluetooth/qlowenergycontroller.h @@ -44,11 +44,16 @@ #include <QtBluetooth/QBluetoothAddress> #include <QtBluetooth/QBluetoothDeviceInfo> #include <QtBluetooth/QBluetoothUuid> +#include <QtBluetooth/QLowEnergyAdvertisingData> #include <QtBluetooth/QLowEnergyService> QT_BEGIN_NAMESPACE +class QLowEnergyAdvertisingParameters; +class QLowEnergyConnectionParameters; class QLowEnergyControllerPrivate; +class QLowEnergyServiceData; + class Q_BLUETOOTH_EXPORT QLowEnergyController : public QObject { Q_OBJECT @@ -59,7 +64,8 @@ public: UnknownRemoteDeviceError, NetworkError, InvalidBluetoothAdapterError, - ConnectionError + ConnectionError, + AdvertisingError, }; Q_ENUM(Error) @@ -69,7 +75,8 @@ public: ConnectedState, DiscoveringState, DiscoveredState, - ClosingState + ClosingState, + AdvertisingState, }; Q_ENUM(ControllerState) @@ -79,6 +86,9 @@ public: }; Q_ENUM(RemoteAddressType) + enum Role { CentralRole, PeripheralRole }; + Q_ENUM(Role) + explicit QLowEnergyController(const QBluetoothAddress &remoteDevice, QObject *parent = 0); // TODO Qt 6 remove ctor explicit QLowEnergyController(const QBluetoothDeviceInfo &remoteDevice, @@ -86,6 +96,13 @@ public: explicit QLowEnergyController(const QBluetoothAddress &remoteDevice, const QBluetoothAddress &localDevice, QObject *parent = 0); // TODO Qt 6 remove ctor + + static QLowEnergyController *createCentral(const QBluetoothDeviceInfo &remoteDevice, + QObject *parent = 0); + static QLowEnergyController *createPeripheral(QObject *parent = 0); + + // TODO: Allow to set connection timeout (disconnect when no data has been exchanged for n seconds). + ~QLowEnergyController(); QBluetoothAddress localAddress() const; @@ -106,9 +123,20 @@ public: QLowEnergyService *createServiceObject( const QBluetoothUuid &service, QObject *parent = 0); + void startAdvertising(const QLowEnergyAdvertisingParameters ¶meters, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData = QLowEnergyAdvertisingData()); + void stopAdvertising(); + + QLowEnergyService *addService(const QLowEnergyServiceData &service, QObject *parent = 0); + + void requestConnectionUpdate(const QLowEnergyConnectionParameters ¶meters); + Error error() const; QString errorString() const; + Role role() const; + Q_SIGNALS: void connected(); void disconnected(); @@ -117,8 +145,11 @@ Q_SIGNALS: void serviceDiscovered(const QBluetoothUuid &newService); void discoveryFinished(); + void connectionUpdated(const QLowEnergyConnectionParameters ¶meters); private: + explicit QLowEnergyController(QObject *parent = 0); // For the peripheral role. + Q_DECLARE_PRIVATE(QLowEnergyController) QLowEnergyControllerPrivate *d_ptr; }; @@ -128,5 +159,6 @@ QT_END_NAMESPACE Q_DECLARE_METATYPE(QLowEnergyController::Error) Q_DECLARE_METATYPE(QLowEnergyController::ControllerState) Q_DECLARE_METATYPE(QLowEnergyController::RemoteAddressType) +Q_DECLARE_METATYPE(QLowEnergyController::Role) #endif // QLOWENERGYCONTROLLER_H diff --git a/src/bluetooth/qlowenergycontroller_android.cpp b/src/bluetooth/qlowenergycontroller_android.cpp index c4a9ddcc..df3aaa54 100644 --- a/src/bluetooth/qlowenergycontroller_android.cpp +++ b/src/bluetooth/qlowenergycontroller_android.cpp @@ -595,4 +595,32 @@ void QLowEnergyControllerPrivate::serviceError( service->setError(errorCode); } +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData) +{ + Q_UNUSED(params); + Q_UNUSED(advertisingData); + Q_UNUSED(scanResponseData); + qCWarning(QT_BT_ANDROID) << "LE advertising not implemented for Android"; +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ + qCWarning(QT_BT_ANDROID) << "LE advertising not implemented for Android"; +} + +void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) +{ + Q_UNUSED(params); + qCWarning(QT_BT_ANDROID) << "Connection update not implemented for Android"; +} + +void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServiceData &service, + QLowEnergyHandle startHandle) +{ + Q_UNUSED(service); + Q_UNUSED(startHandle); +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller_bluez.cpp b/src/bluetooth/qlowenergycontroller_bluez.cpp index 6979b9b4..1562c3f9 100644 --- a/src/bluetooth/qlowenergycontroller_bluez.cpp +++ b/src/bluetooth/qlowenergycontroller_bluez.cpp @@ -40,24 +40,35 @@ #include "qlowenergycontroller_p.h" #include "qbluetoothsocket_p.h" +#include "qleadvertiser_p.h" #include "bluez/bluez_data_p.h" #include "bluez/hcimanager_p.h" #include <QtCore/QLoggingCategory> +#include <QtBluetooth/QBluetoothLocalDevice> #include <QtBluetooth/QBluetoothSocket> +#include <QtBluetooth/QLowEnergyCharacteristicData> +#include <QtBluetooth/QLowEnergyDescriptorData> #include <QtBluetooth/QLowEnergyService> +#include <QtBluetooth/QLowEnergyServiceData> +#include <algorithm> +#include <climits> +#include <cstring> #include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> #define ATTRIBUTE_CHANNEL_ID 4 #define ATT_DEFAULT_LE_MTU 23 #define ATT_MAX_LE_MTU 0x200 -#define GATT_PRIMARY_SERVICE 0x2800 -#define GATT_SECONDARY_SERVICE 0x2801 -#define GATT_INCLUDED_SERVICE 0x2802 -#define GATT_CHARACTERISTIC 0x2803 +#define GATT_PRIMARY_SERVICE quint16(0x2800) +#define GATT_SECONDARY_SERVICE quint16(0x2801) +#define GATT_INCLUDED_SERVICE quint16(0x2802) +#define GATT_CHARACTERISTIC quint16(0x2803) // GATT commands #define ATT_OP_ERROR_RESPONSE 0x1 @@ -65,12 +76,16 @@ #define ATT_OP_EXCHANGE_MTU_RESPONSE 0x3 //receive server MTU #define ATT_OP_FIND_INFORMATION_REQUEST 0x4 //discover individual attribute info #define ATT_OP_FIND_INFORMATION_RESPONSE 0x5 +#define ATT_OP_FIND_BY_TYPE_VALUE_REQUEST 0x6 +#define ATT_OP_FIND_BY_TYPE_VALUE_RESPONSE 0x7 #define ATT_OP_READ_BY_TYPE_REQUEST 0x8 //discover characteristics #define ATT_OP_READ_BY_TYPE_RESPONSE 0x9 #define ATT_OP_READ_REQUEST 0xA //read characteristic & descriptor values #define ATT_OP_READ_RESPONSE 0xB #define ATT_OP_READ_BLOB_REQUEST 0xC //read values longer than MTU-1 #define ATT_OP_READ_BLOB_RESPONSE 0xD +#define ATT_OP_READ_MULTIPLE_REQUEST 0xE +#define ATT_OP_READ_MULTIPLE_RESPONSE 0xF #define ATT_OP_READ_BY_GROUP_REQUEST 0x10 //discover services #define ATT_OP_READ_BY_GROUP_RESPONSE 0x11 #define ATT_OP_WRITE_REQUEST 0x12 //write characteristic with response @@ -83,6 +98,7 @@ #define ATT_OP_HANDLE_VAL_INDICATION 0x1d //informs about value change -> requires reply #define ATT_OP_HANDLE_VAL_CONFIRMATION 0x1e //answer for ATT_OP_HANDLE_VAL_INDICATION #define ATT_OP_WRITE_COMMAND 0x52 //write characteristic without response +#define ATT_OP_SIGNED_WRITE_COMMAND 0xD2 //GATT command sizes in bytes #define ERROR_RESPONSE_HEADER_SIZE 5 @@ -124,6 +140,10 @@ QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) +using namespace QBluetooth; + +const int maxPrepareQueueSize = 1024; + static inline QBluetoothUuid convert_uuid128(const quint128 *p) { quint128 dst_hostOrder, dst_bigEndian; @@ -201,15 +221,44 @@ static void dumpErrorInformation(const QByteArray &response) << "handle:" << handle; } +static int getUuidSize(const QBluetoothUuid &uuid) +{ + return uuid.minimumSize() == 2 ? 2 : 16; +} + +template<typename T> static void putDataAndIncrement(const T &src, char *&dst) +{ + putBtData(src, dst); + dst += sizeof(T); +} +template<> void putDataAndIncrement(const QBluetoothUuid &uuid, char *&dst) +{ + const int uuidSize = getUuidSize(uuid); + if (uuidSize == 2) + putBtData(uuid.toUInt16(), dst); + else + putBtData(uuid.toUInt128(), dst); + dst += uuidSize; +} +template<> void putDataAndIncrement(const QByteArray &value, char *&dst) +{ + using namespace std; + memcpy(dst, value.constData(), value.count()); + dst += value.count(); +} + QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() : QObject(), state(QLowEnergyController::UnconnectedState), error(QLowEnergyController::NoError), + lastLocalHandle(0), l2cpSocket(0), requestPending(false), mtuSize(ATT_DEFAULT_LE_MTU), securityLevelValue(-1), encryptionChangePending(false), - hciManager(0) + hciManager(0), + advertiser(0), + serverSocketNotifier(0) { registerQLowEnergyControllerMetaType(); qRegisterMetaType<QList<QLowEnergyHandle> >(); @@ -221,10 +270,122 @@ QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() hciManager->monitorEvent(HciManager::EncryptChangeEvent); connect(hciManager, SIGNAL(encryptionChangedEvent(QBluetoothAddress,bool)), this, SLOT(encryptionChangedEvent(QBluetoothAddress,bool))); + hciManager->monitorEvent(HciManager::LeMetaEvent); + connect(hciManager, &HciManager::connectionComplete, [this](quint16 handle) { + connectionHandle = handle; + qCDebug(QT_BT_BLUEZ) << "received connection complete event, handle:" << handle; + }); + connect(hciManager, &HciManager::connectionUpdate, + [this](quint16 handle, const QLowEnergyConnectionParameters ¶ms) { + if (handle == connectionHandle) + emit q_ptr->connectionUpdated(params); + } + ); } QLowEnergyControllerPrivate::~QLowEnergyControllerPrivate() { + closeServerSocket(); +} + +class ServerSocket +{ +public: + bool listen(const QBluetoothAddress &localAdapter) + { + m_socket = ::socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (m_socket == -1) { + qCWarning(QT_BT_BLUEZ) << "socket creation failed:" << qt_error_string(errno); + return false; + } + sockaddr_l2 addr; + + // memset should be in std namespace for C++ compilers, but we also need to support + // broken ones that put it in the global one. + using namespace std; + memset(&addr, 0, sizeof addr); + + addr.l2_family = AF_BLUETOOTH; + addr.l2_cid = htobs(ATTRIBUTE_CHANNEL_ID); + addr.l2_bdaddr_type = BDADDR_LE_PUBLIC; + convertAddress(localAdapter.toUInt64(), addr.l2_bdaddr.b); + if (::bind(m_socket, reinterpret_cast<sockaddr *>(&addr), sizeof addr) == -1) { + qCWarning(QT_BT_BLUEZ) << "bind() failed:" << qt_error_string(errno); + return false; + } + if (::listen(m_socket, 1)) { + qCWarning(QT_BT_BLUEZ) << "listen() failed:" << qt_error_string(errno); + return false; + } + return true; + } + + ~ServerSocket() + { + if (m_socket != -1) + close(m_socket); + } + + int takeSocket() + { + const int socket = m_socket; + m_socket = -1; + return socket; + } + +private: + int m_socket = -1; +}; + +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData) +{ + qCDebug(QT_BT_BLUEZ) << "Starting to advertise"; + if (!advertiser) { + advertiser = new QLeAdvertiserBluez(params, advertisingData, scanResponseData, *hciManager, + this); + connect(advertiser, &QLeAdvertiser::errorOccurred, this, + &QLowEnergyControllerPrivate::handleAdvertisingError); + } + setState(QLowEnergyController::AdvertisingState); + advertiser->startAdvertising(); + if (params.mode() == QLowEnergyAdvertisingParameters::AdvNonConnInd + || params.mode() == QLowEnergyAdvertisingParameters::AdvScanInd) { + qCDebug(QT_BT_BLUEZ) << "Non-connectable advertising requested, " + "not listening for connections."; + return; + } + + ServerSocket serverSocket; + if (!serverSocket.listen(localAdapter)) { + setError(QLowEnergyController::AdvertisingError); + setState(QLowEnergyController::UnconnectedState); + return; + } + + const int socketFd = serverSocket.takeSocket(); + serverSocketNotifier = new QSocketNotifier(socketFd, QSocketNotifier::Read, this); + connect(serverSocketNotifier, &QSocketNotifier::activated, this, + &QLowEnergyControllerPrivate::handleConnectionRequest); +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ + setState(QLowEnergyController::UnconnectedState); + advertiser->stopAdvertising(); +} + +void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) +{ + // The spec says that the connection update command can be used by both slave and master + // devices, but BlueZ allows it only for master devices. So for slave devices, we have to use a + // connection parameter update request, which we need to wrap in an ACL command, as BlueZ + // does not allow user-space sockets for the signaling channel. + if (role == QLowEnergyController::CentralRole) + hciManager->sendConnectionUpdateCommand(connectionHandle, params); + else + hciManager->sendConnectionParameterUpdateRequest(connectionHandle, params); } void QLowEnergyControllerPrivate::connectToDevice() @@ -302,7 +463,10 @@ void QLowEnergyControllerPrivate::l2cpDisconnected() { Q_Q(QLowEnergyController); - securityLevelValue = -1; + if (role == QLowEnergyController::PeripheralRole) + storeClientConfigurations(); + invalidateServices(); + resetController(); setState(QLowEnergyController::UnconnectedState); emit q->disconnected(); } @@ -339,23 +503,29 @@ void QLowEnergyControllerPrivate::l2cpErrorChanged(QBluetoothSocket::SocketError void QLowEnergyControllerPrivate::resetController() { openRequests.clear(); + openPrepareWriteRequests.clear(); + scheduledIndications.clear(); + indicationInFlight = false; requestPending = false; encryptionChangePending = false; + receivedMtuExchangeRequest = false; securityLevelValue = -1; + connectionHandle = 0; } void QLowEnergyControllerPrivate::l2cpReadyRead() { - const QByteArray reply = l2cpSocket->readAll(); - qCDebug(QT_BT_BLUEZ) << "Received size:" << reply.size() << "data:" << reply.toHex(); - if (reply.isEmpty()) + const QByteArray incomingPacket = l2cpSocket->readAll(); + qCDebug(QT_BT_BLUEZ) << "Received size:" << incomingPacket.size() << "data:" + << incomingPacket.toHex(); + if (incomingPacket.isEmpty()) return; - const quint8 command = reply.constData()[0]; + const quint8 command = incomingPacket.constData()[0]; switch (command) { case ATT_OP_HANDLE_VAL_NOTIFICATION: { - processUnsolicitedReply(reply); + processUnsolicitedReply(incomingPacket); return; } case ATT_OP_HANDLE_VAL_INDICATION: @@ -363,31 +533,55 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() //send confirmation QByteArray packet; packet.append(static_cast<char>(ATT_OP_HANDLE_VAL_CONFIRMATION)); - sendCommand(packet); + sendPacket(packet); - processUnsolicitedReply(reply); + processUnsolicitedReply(incomingPacket); return; } + case ATT_OP_EXCHANGE_MTU_REQUEST: - case ATT_OP_READ_BY_GROUP_REQUEST: + handleExchangeMtuRequest(incomingPacket); + return; + case ATT_OP_FIND_INFORMATION_REQUEST: + handleFindInformationRequest(incomingPacket); + return; + case ATT_OP_FIND_BY_TYPE_VALUE_REQUEST: + handleFindByTypeValueRequest(incomingPacket); + return; case ATT_OP_READ_BY_TYPE_REQUEST: + handleReadByTypeRequest(incomingPacket); + return; case ATT_OP_READ_REQUEST: - case ATT_OP_FIND_INFORMATION_REQUEST: + handleReadRequest(incomingPacket); + return; + case ATT_OP_READ_BLOB_REQUEST: + handleReadBlobRequest(incomingPacket); + return; + case ATT_OP_READ_MULTIPLE_REQUEST: + handleReadMultipleRequest(incomingPacket); + return; + case ATT_OP_READ_BY_GROUP_REQUEST: + handleReadByGroupTypeRequest(incomingPacket); + return; case ATT_OP_WRITE_REQUEST: - { - qCDebug(QT_BT_BLUEZ) << "Server request" << hex << command; - - //send not supported - QByteArray packet(ERROR_RESPONSE_HEADER_SIZE, Qt::Uninitialized); - packet[0] = ATT_OP_ERROR_RESPONSE; - packet[1] = command; - bt_put_unaligned(htobs(0), (quint16 *)(packet.data() + 2)); - packet[4] = ATT_ERROR_REQUEST_NOT_SUPPORTED; - - sendCommand(packet); - + case ATT_OP_WRITE_COMMAND: + case ATT_OP_SIGNED_WRITE_COMMAND: + handleWriteRequestOrCommand(incomingPacket); + return; + case ATT_OP_PREPARE_WRITE_REQUEST: + handlePrepareWriteRequest(incomingPacket); + return; + 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; @@ -396,7 +590,7 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() Q_ASSERT(!openRequests.isEmpty()); const Request request = openRequests.dequeue(); - processReply(request, reply); + processReply(request, incomingPacket); sendNextPendingRequest(); } @@ -459,12 +653,12 @@ void QLowEnergyControllerPrivate::encryptionChangedEvent( sendNextPendingRequest(); } -void QLowEnergyControllerPrivate::sendCommand(const QByteArray &packet) +void QLowEnergyControllerPrivate::sendPacket(const QByteArray &packet) { qint64 result = l2cpSocket->write(packet.constData(), packet.size()); if (result == -1) { - qCDebug(QT_BT_BLUEZ) << "Cannot write L2CP command:" << hex + qCDebug(QT_BT_BLUEZ) << "Cannot write L2CP packet:" << hex << packet.toHex() << l2cpSocket->errorString(); setError(QLowEnergyController::NetworkError); @@ -481,7 +675,7 @@ void QLowEnergyControllerPrivate::sendNextPendingRequest() // << request.payload.toHex(); requestPending = true; - sendCommand(request.payload); + sendPacket(request.payload); } QLowEnergyHandle parseReadByTypeCharDiscovery( @@ -1078,9 +1272,9 @@ void QLowEnergyControllerPrivate::sendReadByGroupRequest( quint8 packet[GRP_TYPE_REQ_HEADER_SIZE]; packet[0] = ATT_OP_READ_BY_GROUP_REQUEST; - bt_put_unaligned(htobs(start), (quint16 *) &packet[1]); - bt_put_unaligned(htobs(end), (quint16 *) &packet[3]); - bt_put_unaligned(htobs(type), (quint16 *) &packet[5]); + putBtData(start, &packet[1]); + putBtData(end, &packet[3]); + putBtData(type, &packet[5]); QByteArray data(GRP_TYPE_REQ_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, GRP_TYPE_REQ_HEADER_SIZE); @@ -1116,9 +1310,9 @@ void QLowEnergyControllerPrivate::sendReadByTypeRequest( quint8 packet[READ_BY_TYPE_REQ_HEADER_SIZE]; packet[0] = ATT_OP_READ_BY_TYPE_REQUEST; - bt_put_unaligned(htobs(nextHandle), (quint16 *) &packet[1]); - bt_put_unaligned(htobs(serviceData->endHandle), (quint16 *) &packet[3]); - bt_put_unaligned(htobs(attributeType), (quint16 *) &packet[5]); + putBtData(nextHandle, &packet[1]); + putBtData(serviceData->endHandle, &packet[3]); + putBtData(attributeType, &packet[5]); QByteArray data(READ_BY_TYPE_REQ_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, READ_BY_TYPE_REQ_HEADER_SIZE); @@ -1212,7 +1406,7 @@ void QLowEnergyControllerPrivate::readServiceValues( for (int i = 0; i < targetHandles.count(); i++) { pair = targetHandles.at(i); packet[0] = ATT_OP_READ_REQUEST; - bt_put_unaligned(htobs(pair.first), (quint16 *) &packet[1]); + putBtData(pair.first, &packet[1]); QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); @@ -1267,8 +1461,8 @@ void QLowEnergyControllerPrivate::readServiceValuesByOffset( } } - bt_put_unaligned(htobs(handleToRead), (quint16 *) &packet[1]); - bt_put_unaligned(htobs(offset), (quint16 *) &packet[3]); + putBtData(handleToRead, &packet[1]); + putBtData(offset, &packet[3]); QByteArray data(READ_BLOB_REQUEST_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, READ_BLOB_REQUEST_HEADER_SIZE); @@ -1331,7 +1525,7 @@ void QLowEnergyControllerPrivate::exchangeMTU() quint8 packet[MTU_EXCHANGE_HEADER_SIZE]; packet[0] = ATT_OP_EXCHANGE_MTU_REQUEST; - bt_put_unaligned(htobs(ATT_MAX_LE_MTU), (quint16 *) &packet[1]); + putBtData(quint16(ATT_MAX_LE_MTU), &packet[1]); QByteArray data(MTU_EXCHANGE_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, MTU_EXCHANGE_HEADER_SIZE); @@ -1449,8 +1643,8 @@ void QLowEnergyControllerPrivate::discoverNextDescriptor( else charEndHandle = pendingCharHandles[1] - 1; - bt_put_unaligned(htobs(charStartHandle), (quint16 *) &packet[1]); - bt_put_unaligned(htobs(charEndHandle), (quint16 *) &packet[3]); + putBtData(charStartHandle, &packet[1]); + putBtData(charEndHandle, &packet[3]); QByteArray data(FIND_INFO_REQUEST_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, FIND_INFO_REQUEST_HEADER_SIZE); @@ -1485,8 +1679,8 @@ void QLowEnergyControllerPrivate::sendNextPrepareWriteRequest( quint8 packet[PREPARE_WRITE_HEADER_SIZE]; packet[0] = ATT_OP_PREPARE_WRITE_REQUEST; - bt_put_unaligned(htobs(targetHandle), (quint16 *) &packet[1]); // attribute handle - bt_put_unaligned(htobs(offset), (quint16 *) &packet[3]); // offset into newValue + putBtData(targetHandle, &packet[1]); // attribute handle + putBtData(offset, &packet[3]); // offset into newValue qCDebug(QT_BT_BLUEZ) << "Writing long characteristic (prepare):" << hex << handle; @@ -1563,49 +1757,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; + writeCharacteristicForCentral(charHandle, charData.valueHandle, newValue, + writeWithResponse); } - - 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) { - sendCommand(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::writeDescriptor( @@ -1616,32 +1774,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); } /*! @@ -1668,7 +1804,7 @@ void QLowEnergyControllerPrivate::readCharacteristic( quint8 packet[READ_REQUEST_HEADER_SIZE]; packet[0] = ATT_OP_READ_REQUEST; - bt_put_unaligned(htobs(charDetails.valueHandle), (quint16 *) &packet[1]); + putBtData(charDetails.valueHandle, &packet[1]); QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); @@ -1703,7 +1839,7 @@ void QLowEnergyControllerPrivate::readDescriptor( quint8 packet[READ_REQUEST_HEADER_SIZE]; packet[0] = ATT_OP_READ_REQUEST; - bt_put_unaligned(htobs(descriptorHandle), (quint16 *) &packet[1]); + putBtData(descriptorHandle, &packet[1]); QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); @@ -1752,4 +1888,1115 @@ bool QLowEnergyControllerPrivate::increaseEncryptLevelfRequired(quint8 errorCode return false; } +void QLowEnergyControllerPrivate::handleAdvertisingError() +{ + qCWarning(QT_BT_BLUEZ) << "received advertising error"; + setError(QLowEnergyController::AdvertisingError); + setState(QLowEnergyController::UnconnectedState); +} + +bool QLowEnergyControllerPrivate::checkPacketSize(const QByteArray &packet, int minSize, + int maxSize) +{ + if (maxSize == -1) + maxSize = minSize; + if (Q_LIKELY(packet.count() >= minSize && packet.count() <= maxSize)) + return true; + qCWarning(QT_BT_BLUEZ) << "client request of type" << packet.at(0) + << "has unexpected packet size" << packet.count(); + sendErrorResponse(packet.at(0), 0, ATT_ERROR_INVALID_PDU); + return false; +} + +bool QLowEnergyControllerPrivate::checkHandle(const QByteArray &packet, QLowEnergyHandle handle) +{ + if (handle != 0 && handle <= lastLocalHandle) + return true; + sendErrorResponse(packet.at(0), handle, ATT_ERROR_INVALID_HANDLE); + return false; +} + +bool QLowEnergyControllerPrivate::checkHandlePair(quint8 request, QLowEnergyHandle startingHandle, + QLowEnergyHandle endingHandle) +{ + if (startingHandle == 0 || startingHandle > endingHandle) { + qCDebug(QT_BT_BLUEZ) << "handle range invalid"; + sendErrorResponse(request, startingHandle, ATT_ERROR_INVALID_HANDLE); + return false; + } + return true; +} + +void QLowEnergyControllerPrivate::handleExchangeMtuRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.2 + + if (!checkPacketSize(packet, 3)) + return; + if (receivedMtuExchangeRequest) { // Client must only send this once per connection. + qCDebug(QT_BT_BLUEZ) << "Client sent extraneous MTU exchange packet"; + sendErrorResponse(packet.at(0), 0, ATT_ERROR_REQUEST_NOT_SUPPORTED); + return; + } + receivedMtuExchangeRequest = true; + + // Send reply. + QByteArray reply(MTU_EXCHANGE_HEADER_SIZE, Qt::Uninitialized); + reply[0] = ATT_OP_EXCHANGE_MTU_RESPONSE; + putBtData(static_cast<quint16>(ATT_MAX_LE_MTU), reply.data() + 1); + sendPacket(reply); + + // Apply requested MTU. + const quint16 clientRxMtu = bt_get_le16(packet.constData() + 1); + mtuSize = qMax<quint16>(ATT_DEFAULT_LE_MTU, qMin<quint16>(clientRxMtu, ATT_MAX_LE_MTU)); + qCDebug(QT_BT_BLUEZ) << "MTU request from client:" << clientRxMtu + << "effective client RX MTU:" << mtuSize; + qCDebug(QT_BT_BLUEZ) << "Sending server RX MTU" << ATT_MAX_LE_MTU; +} + +void QLowEnergyControllerPrivate::handleFindInformationRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.3.1-2 + + if (!checkPacketSize(packet, 5)) + return; + const QLowEnergyHandle startingHandle = bt_get_le16(packet.constData() + 1); + const QLowEnergyHandle endingHandle = bt_get_le16(packet.constData() + 3); + qCDebug(QT_BT_BLUEZ) << "client sends find information request; start:" << startingHandle + << "end:" << endingHandle; + if (!checkHandlePair(packet.at(0), startingHandle, endingHandle)) + return; + + QVector<Attribute> results = getAttributes(startingHandle, endingHandle); + if (results.isEmpty()) { + sendErrorResponse(packet.at(0), startingHandle, ATT_ERROR_ATTRIBUTE_NOT_FOUND); + return; + } + ensureUniformUuidSizes(results); + + QByteArray responsePrefix(2, Qt::Uninitialized); + const int uuidSize = getUuidSize(results.first().type); + responsePrefix[0] = ATT_OP_FIND_INFORMATION_RESPONSE; + responsePrefix[1] = uuidSize == 2 ? 0x1 : 0x2; + const int elementSize = sizeof(QLowEnergyHandle) + uuidSize; + const auto elemWriter = [](const Attribute &attr, char *&data) { + putDataAndIncrement(attr.handle, data); + putDataAndIncrement(attr.type, data); + }; + sendListResponse(responsePrefix, elementSize, results, elemWriter); + +} + +void QLowEnergyControllerPrivate::handleFindByTypeValueRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.3.3-4 + + if (!checkPacketSize(packet, 7, mtuSize)) + return; + const QLowEnergyHandle startingHandle = bt_get_le16(packet.constData() + 1); + const QLowEnergyHandle endingHandle = bt_get_le16(packet.constData() + 3); + const quint16 type = bt_get_le16(packet.constData() + 5); + const QByteArray value = QByteArray::fromRawData(packet.constData() + 7, packet.count() - 7); + qCDebug(QT_BT_BLUEZ) << "client sends find by type value request; start:" << startingHandle + << "end:" << endingHandle << "type:" << type + << "value:" << value.toHex(); + if (!checkHandlePair(packet.at(0), startingHandle, endingHandle)) + return; + + const auto predicate = [value, this, type](const Attribute &attr) { + return attr.type == QBluetoothUuid(type) && attr.value == value + && checkReadPermissions(attr) == 0; + }; + const QVector<Attribute> results = getAttributes(startingHandle, endingHandle, predicate); + if (results.isEmpty()) { + sendErrorResponse(packet.at(0), startingHandle, ATT_ERROR_ATTRIBUTE_NOT_FOUND); + return; + } + + QByteArray responsePrefix(1, ATT_OP_FIND_BY_TYPE_VALUE_RESPONSE); + const int elemSize = 2 * sizeof(QLowEnergyHandle); + const auto elemWriter = [](const Attribute &attr, char *&data) { + putDataAndIncrement(attr.handle, data); + putDataAndIncrement(attr.groupEndHandle, data); + }; + sendListResponse(responsePrefix, elemSize, results, elemWriter); +} + +void QLowEnergyControllerPrivate::handleReadByTypeRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.4.1-2 + + if (!checkPacketSize(packet, 7, 21)) + return; + const QLowEnergyHandle startingHandle = bt_get_le16(packet.constData() + 1); + const QLowEnergyHandle endingHandle = bt_get_le16(packet.constData() + 3); + const void * const typeStart = packet.constData() + 5; + const bool is16BitUuid = packet.count() == 7; + const bool is128BitUuid = packet.count() == 21; + QBluetoothUuid type; + if (is16BitUuid) { + type = QBluetoothUuid(bt_get_le16(typeStart)); + } else if (is128BitUuid) { + type = QBluetoothUuid(convert_uuid128(reinterpret_cast<const quint128 *>(typeStart))); + } else { + qCWarning(QT_BT_BLUEZ) << "read by type request has invalid packet size" << packet.count(); + sendErrorResponse(packet.at(0), 0, ATT_ERROR_INVALID_PDU); + return; + } + qCDebug(QT_BT_BLUEZ) << "client sends read by type request, start:" << startingHandle + << "end:" << endingHandle << "type:" << type; + if (!checkHandlePair(packet.at(0), startingHandle, endingHandle)) + return; + + // Get all attributes with matching type. + QVector<Attribute> results = getAttributes(startingHandle, endingHandle, + [type](const Attribute &attr) { return attr.type == type; }); + ensureUniformValueSizes(results); + + if (results.isEmpty()) { + sendErrorResponse(packet.at(0), startingHandle, ATT_ERROR_ATTRIBUTE_NOT_FOUND); + return; + } + + const int error = checkReadPermissions(results); + if (error) { + sendErrorResponse(packet.at(0), results.first().handle, error); + return; + } + + const int elementSize = sizeof(QLowEnergyHandle) + results.first().value.count(); + QByteArray responsePrefix(2, Qt::Uninitialized); + responsePrefix[0] = ATT_OP_READ_BY_TYPE_RESPONSE; + responsePrefix[1] = elementSize; + const auto elemWriter = [](const Attribute &attr, char *&data) { + putDataAndIncrement(attr.handle, data); + putDataAndIncrement(attr.value, data); + }; + sendListResponse(responsePrefix, elementSize, results, elemWriter); +} + +void QLowEnergyControllerPrivate::handleReadRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.4.3-4 + + if (!checkPacketSize(packet, 3)) + return; + const QLowEnergyHandle handle = bt_get_le16(packet.constData() + 1); + qCDebug(QT_BT_BLUEZ) << "client sends read request; handle:" << handle; + + if (!checkHandle(packet, handle)) + return; + const Attribute &attribute = localAttributes.at(handle); + const int permissionsError = checkReadPermissions(attribute); + if (permissionsError) { + sendErrorResponse(packet.at(0), handle, permissionsError); + return; + } + + const int sentValueLength = qMin(attribute.value.count(), mtuSize - 1); + QByteArray response(1 + sentValueLength, Qt::Uninitialized); + response[0] = ATT_OP_READ_RESPONSE; + using namespace std; + memcpy(response.data() + 1, attribute.value.constData(), sentValueLength); + qCDebug(QT_BT_BLUEZ) << "sending response:" << response.toHex(); + sendPacket(response); +} + +void QLowEnergyControllerPrivate::handleReadBlobRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.4.5-6 + + if (!checkPacketSize(packet, 5)) + return; + const QLowEnergyHandle handle = bt_get_le16(packet.constData() + 1); + const quint16 valueOffset = bt_get_le16(packet.constData() + 3); + qCDebug(QT_BT_BLUEZ) << "client sends read blob request; handle:" << handle + << "offset:" << valueOffset; + + if (!checkHandle(packet, handle)) + return; + const Attribute &attribute = localAttributes.at(handle); + const int permissionsError = checkReadPermissions(attribute); + if (permissionsError) { + sendErrorResponse(packet.at(0), handle, permissionsError); + return; + } + if (valueOffset > attribute.value.count()) { + sendErrorResponse(packet.at(0), handle, ATT_ERROR_INVALID_OFFSET); + return; + } + if (attribute.value.count() <= mtuSize - 3) { + sendErrorResponse(packet.at(0), handle, ATT_ERROR_ATTRIBUTE_NOT_LONG); + return; + } + + // Yes, this value can be zero. + const int sentValueLength = qMin(attribute.value.count() - valueOffset, mtuSize - 1); + + QByteArray response(1 + sentValueLength, Qt::Uninitialized); + response[0] = ATT_OP_READ_BLOB_RESPONSE; + using namespace std; + memcpy(response.data() + 1, attribute.value.constData() + valueOffset, sentValueLength); + qCDebug(QT_BT_BLUEZ) << "sending response:" << response.toHex(); + sendPacket(response); +} + +void QLowEnergyControllerPrivate::handleReadMultipleRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.4.7-8 + + if (!checkPacketSize(packet, 5, mtuSize)) + return; + QVector<QLowEnergyHandle> handles((packet.count() - 1) / sizeof(QLowEnergyHandle)); + auto *packetPtr = reinterpret_cast<const QLowEnergyHandle *>(packet.constData() + 1); + for (int i = 0; i < handles.count(); ++i, ++packetPtr) + handles[i] = bt_get_le16(packetPtr); + qCDebug(QT_BT_BLUEZ) << "client sends read multiple request for handles" << handles; + + const auto it = std::find_if(handles.constBegin(), handles.constEnd(), + [this](QLowEnergyHandle handle) { return handle >= lastLocalHandle; }); + if (it != handles.constEnd()) { + sendErrorResponse(packet.at(0), *it, ATT_ERROR_INVALID_HANDLE); + return; + } + const QVector<Attribute> results = getAttributes(handles.first(), handles.last()); + QByteArray response(1, ATT_OP_READ_MULTIPLE_RESPONSE); + foreach (const Attribute &attr, results) { + const int error = checkReadPermissions(attr); + if (error) { + sendErrorResponse(packet.at(0), attr.handle, error); + return; + } + + // Note: We do not abort if no more values fit into the packet, because we still have to + // report possible permission errors for the other handles. + response += attr.value.left(mtuSize - response.count()); + } + + qCDebug(QT_BT_BLUEZ) << "sending response:" << response.toHex(); + sendPacket(response); +} + +void QLowEnergyControllerPrivate::handleReadByGroupTypeRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.4.9-10 + + if (!checkPacketSize(packet, 7, 21)) + return; + const QLowEnergyHandle startingHandle = bt_get_le16(packet.constData() + 1); + const QLowEnergyHandle endingHandle = bt_get_le16(packet.constData() + 3); + const bool is16BitUuid = packet.count() == 7; + const bool is128BitUuid = packet.count() == 21; + const void * const typeStart = packet.constData() + 5; + QBluetoothUuid type; + if (is16BitUuid) { + type = QBluetoothUuid(bt_get_le16(typeStart)); + } else if (is128BitUuid) { + type = QBluetoothUuid(convert_uuid128(reinterpret_cast<const quint128 *>(typeStart))); + } else { + qCWarning(QT_BT_BLUEZ) << "read by group type request has invalid packet size" + << packet.count(); + sendErrorResponse(packet.at(0), 0, ATT_ERROR_INVALID_PDU); + return; + } + qCDebug(QT_BT_BLUEZ) << "client sends read by group type request, start:" << startingHandle + << "end:" << endingHandle << "type:" << type; + + if (!checkHandlePair(packet.at(0), startingHandle, endingHandle)) + return; + if (type != QBluetoothUuid(static_cast<quint16>(GATT_PRIMARY_SERVICE)) + && type != QBluetoothUuid(static_cast<quint16>(GATT_SECONDARY_SERVICE))) { + sendErrorResponse(packet.at(0), startingHandle, ATT_ERROR_UNSUPPRTED_GROUP_TYPE); + return; + } + + QVector<Attribute> results = getAttributes(startingHandle, endingHandle, + [type](const Attribute &attr) { return attr.type == type; }); + if (results.isEmpty()) { + sendErrorResponse(packet.at(0), startingHandle, ATT_ERROR_ATTRIBUTE_NOT_FOUND); + return; + } + const int error = checkReadPermissions(results); + if (error) { + sendErrorResponse(packet.at(0), results.first().handle, error); + return; + } + + ensureUniformValueSizes(results); + + const int elementSize = 2 * sizeof(QLowEnergyHandle) + results.first().value.count(); + QByteArray responsePrefix(2, Qt::Uninitialized); + responsePrefix[0] = ATT_OP_READ_BY_GROUP_RESPONSE; + responsePrefix[1] = elementSize; + const auto elemWriter = [](const Attribute &attr, char *&data) { + putDataAndIncrement(attr.handle, data); + putDataAndIncrement(attr.groupEndHandle, data); + putDataAndIncrement(attr.value, data); + }; + 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; + } + + putBtData(valueHandle, &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; + putBtData(descriptorHandle, &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 + + const bool isRequest = packet.at(0) == ATT_OP_WRITE_REQUEST; + const bool isSigned = quint8(packet.at(0)) == quint8(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; + + if (!checkHandle(packet, handle)) + return; + + Attribute &attribute = localAttributes[handle]; + const QLowEnergyCharacteristic::PropertyType type = isRequest + ? QLowEnergyCharacteristic::Write : isSigned + ? QLowEnergyCharacteristic::WriteSigned : QLowEnergyCharacteristic::WriteNoResponse; + const int permissionsError = checkPermissions(attribute, type); + if (permissionsError) { + sendErrorResponse(packet.at(0), handle, permissionsError); + return; + } + + int valueLength; + if (isSigned) { + if (!isBonded()) { + qCWarning(QT_BT_BLUEZ) << "Ignoring signed write from non-bonded device."; + return; + } + if (securityLevel() >= BT_SECURITY_MEDIUM) { + qCWarning(QT_BT_BLUEZ) << "Ignoring signed write on encrypted link."; + return; + } + // const QByteArray signature = packet.right(12); + qCWarning(QT_BT_BLUEZ) << "signed write not implemented, ignoring."; + return; // TODO: Check signature and continue if it's valid. Check and update sign counter. + valueLength = packet.count() - 15; + } else { + valueLength = packet.count() - 3; + } + + if (valueLength > attribute.maxLength) { + sendErrorResponse(packet.at(0), handle, ATT_ERROR_INVAL_ATTR_VALUE_LEN); + return; + } + + // 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); + + 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) +{ + // Spec v4.2, Vol 3, Part F, 3.4.6.1 + + if (!checkPacketSize(packet, 5, mtuSize)) + return; + const quint16 handle = bt_get_le16(packet.constData() + 1); + qCDebug(QT_BT_BLUEZ) << "client sends prepare write request for handle" << handle; + + if (!checkHandle(packet, handle)) + return; + const Attribute &attribute = localAttributes.at(handle); + const int permissionsError = checkPermissions(attribute, QLowEnergyCharacteristic::Write); + if (permissionsError) { + sendErrorResponse(packet.at(0), handle, permissionsError); + return; + } + if (openPrepareWriteRequests.count() >= maxPrepareQueueSize) { + sendErrorResponse(packet.at(0), handle, ATT_ERROR_PREPARE_QUEUE_FULL); + return; + } + + // The value is not checked here, but on the Execute request. + openPrepareWriteRequests << WriteRequest(handle, bt_get_le16(packet.constData() + 3), + packet.mid(5)); + + QByteArray response = packet; + response[0] = ATT_OP_PREPARE_WRITE_RESPONSE; + sendPacket(response); +} + +void QLowEnergyControllerPrivate::handleExecuteWriteRequest(const QByteArray &packet) +{ + // Spec v4.2, Vol 3, Part F, 3.4.6.3 + + if (!checkPacketSize(packet, 2)) + return; + const bool cancel = packet.at(1) == 0; + qCDebug(QT_BT_BLUEZ) << "client sends execute write request; flag is" + << (cancel ? "cancel" : "flush"); + + QVector<WriteRequest> requests = openPrepareWriteRequests; + openPrepareWriteRequests.clear(); + QVector<QLowEnergyCharacteristic> characteristics; + QVector<QLowEnergyDescriptor> descriptors; + if (!cancel) { + 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; + } + } + } + + 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) +{ + // An ATT command never receives an error response. + if (request == ATT_OP_WRITE_COMMAND || request == ATT_OP_SIGNED_WRITE_COMMAND) + return; + + QByteArray packet(ERROR_RESPONSE_HEADER_SIZE, Qt::Uninitialized); + packet[0] = ATT_OP_ERROR_RESPONSE; + packet[1] = request; + putBtData(handle, packet.data() + 2); + packet[4] = code; + qCWarning(QT_BT_BLUEZ) << "sending error response; request:" << request << "handle:" << handle + << "code:" << code; + sendPacket(packet); +} + +void QLowEnergyControllerPrivate::sendListResponse(const QByteArray &packetStart, int elemSize, + const QVector<Attribute> &attributes, const ElemWriter &elemWriter) +{ + const int offset = packetStart.count(); + const int elemCount = qMin(attributes.count(), (mtuSize - offset) / elemSize); + const int totalPacketSize = offset + elemCount * elemSize; + QByteArray response(totalPacketSize, Qt::Uninitialized); + using namespace std; + memcpy(response.data(), packetStart.constData(), offset); + char *data = response.data() + offset; + for_each(attributes.constBegin(), attributes.constBegin() + elemCount, + [&data, elemWriter](const Attribute &attr) { elemWriter(attr, data); }); + qCDebug(QT_BT_BLUEZ) << "sending response:" << response.toHex(); + 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) { + qCWarning(QT_BT_BLUEZ) << "Incoming connection request in unexpected state" << state; + return; + } + Q_ASSERT(serverSocketNotifier); + serverSocketNotifier->setEnabled(false); + sockaddr_l2 clientAddr; + socklen_t clientAddrSize = sizeof clientAddr; + const int clientSocket = accept(serverSocketNotifier->socket(), + reinterpret_cast<sockaddr *>(&clientAddr), &clientAddrSize); + if (clientSocket == -1) { + // Not fatal in itself. The next one might succeed. + qCWarning(QT_BT_BLUEZ) << "accept() failed:" << qt_error_string(errno); + serverSocketNotifier->setEnabled(true); + return; + } + remoteDevice = QBluetoothAddress(convertAddress(clientAddr.l2_bdaddr.b)); + qCDebug(QT_BT_BLUEZ) << "GATT connection from device" << remoteDevice; + if (connectionHandle == 0) + qCWarning(QT_BT_BLUEZ) << "Received client connection, but no connection complete event"; + + closeServerSocket(); + l2cpSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol, this); + connect(l2cpSocket, &QBluetoothSocket::disconnected, + this, &QLowEnergyControllerPrivate::l2cpDisconnected); + connect(l2cpSocket, static_cast<void (QBluetoothSocket::*)(QBluetoothSocket::SocketError)> + (&QBluetoothSocket::error), this, &QLowEnergyControllerPrivate::l2cpErrorChanged); + 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, + QBluetoothSocket::ConnectedState, QIODevice::ReadWrite | QIODevice::Unbuffered); + restoreClientConfigurations(); + setState(QLowEnergyController::ConnectedState); +} + +void QLowEnergyControllerPrivate::closeServerSocket() +{ + if (!serverSocketNotifier) + return; + serverSocketNotifier->disconnect(); + close(serverSocketNotifier->socket()); + serverSocketNotifier->deleteLater(); + 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; + if (uuid.minimumSize() == 2) { + ba.resize(2); + putBtData(uuid.toUInt16(), ba.data()); + } else { + ba.resize(16); + putBtData(uuid.toUInt128(), ba.data()); + } + return ba; +} + +void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServiceData &service, + QLowEnergyHandle startHandle) +{ + // Construct generic attribute data for the service with handles as keys. + // Otherwise a number of request handling functions will be awkward to write + // as well as computationally inefficient. + + localAttributes.resize(lastLocalHandle + 1); + Attribute serviceAttribute; + serviceAttribute.handle = startHandle; + serviceAttribute.type = QBluetoothUuid(static_cast<quint16>(service.type())); + serviceAttribute.properties = QLowEnergyCharacteristic::Read; + serviceAttribute.value = uuidToByteArray(service.uuid()); + QLowEnergyHandle currentHandle = startHandle; + foreach (const QLowEnergyService * const service, service.includedServices()) { + Attribute attribute; + attribute.handle = ++currentHandle; + attribute.type = QBluetoothUuid(GATT_INCLUDED_SERVICE); + attribute.properties = QLowEnergyCharacteristic::Read; + const bool includeUuidInValue = service->serviceUuid().minimumSize() == 2; + attribute.value.resize((2 + includeUuidInValue) * sizeof(QLowEnergyHandle)); + char *valueData = attribute.value.data(); + putDataAndIncrement(service->d_ptr->startHandle, valueData); + putDataAndIncrement(service->d_ptr->endHandle, valueData); + if (includeUuidInValue) + putDataAndIncrement(service->serviceUuid(), valueData); + localAttributes[attribute.handle] = attribute; + } + foreach (const QLowEnergyCharacteristicData &cd, service.characteristics()) { + Attribute attribute; + + // Characteristic declaration; + attribute.handle = ++currentHandle; + attribute.groupEndHandle = attribute.handle + 1 + cd.descriptors().count(); + attribute.type = QBluetoothUuid(GATT_CHARACTERISTIC); + attribute.properties = QLowEnergyCharacteristic::Read; + attribute.value.resize(1 + sizeof(QLowEnergyHandle) + cd.uuid().minimumSize()); + char *valueData = attribute.value.data(); + putDataAndIncrement(static_cast<quint8>(cd.properties()), valueData); + putDataAndIncrement(QLowEnergyHandle(currentHandle + 1), valueData); + putDataAndIncrement(cd.uuid(), valueData); + localAttributes[attribute.handle] = attribute; + + // Characteristic value declaration. + attribute.handle = ++currentHandle; + attribute.groupEndHandle = attribute.handle; + attribute.type = cd.uuid(); + attribute.properties = cd.properties(); + 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()) { + attribute.handle = ++currentHandle; + attribute.groupEndHandle = attribute.handle; + attribute.type = dd.uuid(); + 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.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 + | QLowEnergyCharacteristic::Write + | QLowEnergyCharacteristic::WriteNoResponse + | QLowEnergyCharacteristic::WriteSigned; + attribute.writeConstraints = dd.writeConstraints(); + attribute.minLength = attribute.maxLength = 2; + } else { + if (dd.isReadable()) + attribute.properties |= QLowEnergyCharacteristic::Read; + if (dd.isWritable()) { + attribute.properties |= QLowEnergyCharacteristic::Write; + attribute.properties |= QLowEnergyCharacteristic::WriteNoResponse; + attribute.properties |= QLowEnergyCharacteristic::WriteSigned; + } + attribute.readConstraints = dd.readConstraints(); + attribute.writeConstraints = dd.writeConstraints(); + } + + 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; + } + } + serviceAttribute.groupEndHandle = currentHandle; + localAttributes[serviceAttribute.handle] = serviceAttribute; +} + +void QLowEnergyControllerPrivate::ensureUniformAttributes(QVector<Attribute> &attributes, + const std::function<int (const Attribute &)> &getSize) +{ + if (attributes.isEmpty()) + return; + const int firstSize = getSize(attributes.first()); + const auto it = std::find_if(attributes.begin() + 1, attributes.end(), + [firstSize, getSize](const Attribute &attr) { return getSize(attr) != firstSize; }); + if (it != attributes.end()) + attributes.erase(it, attributes.end()); + +} + +void QLowEnergyControllerPrivate::ensureUniformUuidSizes(QVector<Attribute> &attributes) +{ + ensureUniformAttributes(attributes, + [](const Attribute &attr) { return getUuidSize(attr.type); }); +} + +void QLowEnergyControllerPrivate::ensureUniformValueSizes(QVector<Attribute> &attributes) +{ + ensureUniformAttributes(attributes, + [](const Attribute &attr) { return attr.value.count(); }); +} + +QVector<QLowEnergyControllerPrivate::Attribute> QLowEnergyControllerPrivate::getAttributes(QLowEnergyHandle startHandle, + QLowEnergyHandle endHandle, const AttributePredicate &attributePredicate) +{ + QVector<Attribute> results; + if (startHandle > lastLocalHandle) + return results; + if (lastLocalHandle == 0) // We have no services at all. + return results; + Q_ASSERT(startHandle <= endHandle); // Must have been checked before. + const QLowEnergyHandle firstHandle = qMin(startHandle, lastLocalHandle); + const QLowEnergyHandle lastHandle = qMin(endHandle, lastLocalHandle); + for (QLowEnergyHandle i = firstHandle; i <= lastHandle; ++i) { + const Attribute &attr = localAttributes.at(i); + if (attributePredicate(attr)) + results << attr; + } + return results; +} + +int QLowEnergyControllerPrivate::checkPermissions(const Attribute &attr, + QLowEnergyCharacteristic::PropertyType type) +{ + const bool isReadAccess = type == QLowEnergyCharacteristic::Read; + const bool isWriteCommand = type == QLowEnergyCharacteristic::WriteNoResponse; + const bool isWriteAccess = type == QLowEnergyCharacteristic::Write + || type == QLowEnergyCharacteristic::WriteSigned + || isWriteCommand; + Q_ASSERT(isReadAccess || isWriteAccess); + if (!(attr.properties & type)) { + if (isReadAccess) + return ATT_ERROR_READ_NOT_PERM; + + // The spec says: If an attribute requires a signed write, then a non-signed write command + // can also be used if the link is encrypted. + const bool unsignedWriteOk = isWriteCommand + && (attr.properties & QLowEnergyCharacteristic::WriteSigned) + && securityLevel() >= BT_SECURITY_MEDIUM; + if (!unsignedWriteOk) + return ATT_ERROR_WRITE_NOT_PERM; + } + const AttAccessConstraints constraints = isReadAccess + ? attr.readConstraints : attr.writeConstraints; + if (constraints.testFlag(AttAuthorizationRequired)) + return ATT_ERROR_INSUF_AUTHORIZATION; // TODO: emit signal (and offer authorization function)? + if (constraints.testFlag(AttEncryptionRequired) && securityLevel() < BT_SECURITY_MEDIUM) + return ATT_ERROR_INSUF_ENCRYPTION; + if (constraints.testFlag(AttAuthenticationRequired) && securityLevel() < BT_SECURITY_HIGH) + return ATT_ERROR_INSUF_AUTHENTICATION; + if (false) + return ATT_ERROR_INSUF_ENCR_KEY_SIZE; + return 0; +} + +int QLowEnergyControllerPrivate::checkReadPermissions(const Attribute &attr) +{ + return checkPermissions(attr, QLowEnergyCharacteristic::Read); +} + +int QLowEnergyControllerPrivate::checkReadPermissions(QVector<Attribute> &attributes) +{ + if (attributes.isEmpty()) + return 0; + + // The logic prescribed in the spec is as follows: + // 1) If the first in a list of matching attributes has a permissions error, + // then that error is returned via an error response. + // 2) If any other element of that list would cause a permissions error, then all + // attributes from this one on are not part of the result set, but no error is returned. + const int error = checkReadPermissions(attributes.first()); + if (error) + return error; + const auto it = std::find_if(attributes.begin() + 1, attributes.end(), + [this](const Attribute &attr) { return checkReadPermissions(attr) != 0; }); + if (it != attributes.end()) + attributes.erase(it, attributes.end()); + return 0; +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller_osx.mm b/src/bluetooth/qlowenergycontroller_osx.mm index 6c57c135..62958c11 100644 --- a/src/bluetooth/qlowenergycontroller_osx.mm +++ b/src/bluetooth/qlowenergycontroller_osx.mm @@ -939,6 +939,7 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres { OSX_D_PTR; + osx_d_ptr->role = CentralRole; osx_d_ptr->remoteAddress = remoteAddress; osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); @@ -953,6 +954,7 @@ QLowEnergyController::QLowEnergyController(const QBluetoothDeviceInfo &remoteDev { OSX_D_PTR; + osx_d_ptr->role = CentralRole; osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); // That's the only "real" ctor - with Core Bluetooth we need a _valid_ deviceUuid // from 'remoteDevice'. @@ -966,6 +968,7 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres { OSX_D_PTR; + osx_d_ptr->role = CentralRole; osx_d_ptr->remoteAddress = remoteAddress; osx_d_ptr->localAddress = localAddress; @@ -973,12 +976,39 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres "addresses is not supported!"; } +QLowEnergyController::QLowEnergyController(QObject *parent) + : QObject(parent), d_ptr(new QLowEnergyControllerPrivateOSX(this)) +{ + OSX_D_PTR; + + osx_d_ptr->role = PeripheralRole; + osx_d_ptr->localAddress = QBluetoothLocalDevice().address(); +} + +QLowEnergyController *QLowEnergyController::createCentral(const QBluetoothDeviceInfo &remoteDevice, + QObject *parent) +{ + return new QLowEnergyController(remoteDevice, parent); +} + +QLowEnergyController *QLowEnergyController::createPeripheral(QObject *parent) +{ + return new QLowEnergyController(parent); +} + QLowEnergyController::~QLowEnergyController() { // Deleting a peripheral will also disconnect. delete d_ptr; } +QLowEnergyController::Role QLowEnergyController::role() const +{ + OSX_D_PTR; + + return osx_d_ptr->role; +} + QBluetoothAddress QLowEnergyController::localAddress() const { OSX_D_PTR; @@ -1125,6 +1155,36 @@ QString QLowEnergyController::errorString() const return osx_d_ptr->errorString; } +void QLowEnergyController::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData) +{ + Q_UNUSED(params); + Q_UNUSED(advertisingData); + Q_UNUSED(scanResponseData); + qCWarning(QT_BT_OSX) << "LE advertising not implemented for OS X"; +} + +void QLowEnergyController::stopAdvertising() +{ + qCWarning(QT_BT_OSX) << "LE advertising not implemented for OS X"; +} + +QLowEnergyService *QLowEnergyController::addService(const QLowEnergyServiceData &service, + QObject *parent) +{ + Q_UNUSED(service); + Q_UNUSED(parent); + qCWarning(QT_BT_OSX) << "GATT server functionality not implemented for OS X"; + return nullptr; +} + +void QLowEnergyController::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) +{ + Q_UNUSED(params); + qCWarning(QT_BT_OSX) << "Connection update not implemented for OS X"; +} + QT_END_NAMESPACE #include "moc_qlowenergycontroller_osx_p.cpp" diff --git a/src/bluetooth/qlowenergycontroller_osx_p.h b/src/bluetooth/qlowenergycontroller_osx_p.h index 55b8055f..6dc27aac 100644 --- a/src/bluetooth/qlowenergycontroller_osx_p.h +++ b/src/bluetooth/qlowenergycontroller_osx_p.h @@ -161,6 +161,8 @@ private: QBluetoothAddress localAddress; QBluetoothAddress remoteAddress; + QLowEnergyController::Role role; + QLowEnergyController::ControllerState controllerState; QLowEnergyController::RemoteAddressType addressType; diff --git a/src/bluetooth/qlowenergycontroller_p.cpp b/src/bluetooth/qlowenergycontroller_p.cpp index d773a704..eae0a43c 100644 --- a/src/bluetooth/qlowenergycontroller_p.cpp +++ b/src/bluetooth/qlowenergycontroller_p.cpp @@ -44,7 +44,8 @@ QT_BEGIN_NAMESPACE QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() : QObject(), state(QLowEnergyController::UnconnectedState), - error(QLowEnergyController::NoError) + error(QLowEnergyController::NoError), + lastLocalHandle(0) { registerQLowEnergyControllerMetaType(); } @@ -111,4 +112,23 @@ void QLowEnergyControllerPrivate::writeDescriptor( } +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters &/* params */, + const QLowEnergyAdvertisingData &/* advertisingData */, + const QLowEnergyAdvertisingData &/* scanResponseData */) +{ +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ +} + +void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnectionParameters & /* params */) +{ +} + +void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServiceData &/* service */, + QLowEnergyHandle /* startHandle */) +{ +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller_p.h b/src/bluetooth/qlowenergycontroller_p.h index 2cea7283..095f838f 100644 --- a/src/bluetooth/qlowenergycontroller_p.h +++ b/src/bluetooth/qlowenergycontroller_p.h @@ -71,7 +71,9 @@ QT_END_NAMESPACE #include <qglobal.h> #include <QtCore/QQueue> +#include <QtCore/QVector> #include <QtBluetooth/qbluetooth.h> +#include <QtBluetooth/qlowenergycharacteristic.h> #include "qlowenergycontroller.h" #include "qlowenergyserviceprivate_p.h" @@ -82,10 +84,15 @@ QT_END_NAMESPACE #include "android/lowenergynotificationhub_p.h" #endif +#include <functional> + QT_BEGIN_NAMESPACE +class QLowEnergyServiceData; + #if defined(QT_BLUEZ_BLUETOOTH) && !defined(QT_BLUEZ_NO_BTLE) class HciManager; +class QSocketNotifier; #elif defined(QT_ANDROID_BLUETOOTH) class LowEnergyNotificationHub; #endif @@ -93,6 +100,7 @@ class LowEnergyNotificationHub; extern void registerQLowEnergyControllerMetaType(); typedef QMap<QBluetoothUuid, QSharedPointer<QLowEnergyServicePrivate> > ServiceDataMap; +class QLeAdvertiser; class QLowEnergyControllerPrivate : public QObject { @@ -115,6 +123,13 @@ public: void discoverServiceDetails(const QBluetoothUuid &service); + void startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData); + void stopAdvertising(); + + void requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms); + // misc helpers QSharedPointer<QLowEnergyServicePrivate> serviceForHandle( QLowEnergyHandle handle); @@ -147,9 +162,12 @@ public: const QLowEnergyHandle descriptorHandle, const QByteArray &newValue); + void addToGenericAttributeList(const QLowEnergyServiceData &service, + QLowEnergyHandle startHandle); QBluetoothAddress remoteDevice; QBluetoothAddress localAdapter; + QLowEnergyController::Role role; QString remoteName; @@ -160,10 +178,29 @@ public: // list of all found service uuids ServiceDataMap serviceList; + QLowEnergyHandle lastLocalHandle; + ServiceDataMap localServices; + + struct Attribute { + Attribute() : handle(0) {} + + QLowEnergyHandle handle; + QLowEnergyHandle groupEndHandle; + QLowEnergyCharacteristic::PropertyTypes properties; + QBluetooth::AttAccessConstraints readConstraints; + QBluetooth::AttAccessConstraints writeConstraints; + QBluetoothUuid type; + QByteArray value; + int minLength; + int maxLength; + }; + QVector<Attribute> localAttributes; + QLowEnergyController::RemoteAddressType addressType; private: #if defined(QT_BLUEZ_BLUETOOTH) && !defined(QT_BLUEZ_NO_BTLE) + quint16 connectionHandle = 0; QBluetoothSocket *l2cpSocket; struct Request { quint8 command; @@ -174,14 +211,62 @@ private: QVariant reference2; }; QQueue<Request> openRequests; + + struct WriteRequest { + WriteRequest() {} + WriteRequest(quint16 h, quint16 o, const QByteArray &v) + : handle(h), valueOffset(o), value(v) {} + quint16 handle; + quint16 valueOffset; + QByteArray value; + }; + 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; bool encryptionChangePending; + bool receivedMtuExchangeRequest = false; HciManager *hciManager; + QLeAdvertiser *advertiser; + QSocketNotifier *serverSocketNotifier; + + void handleConnectionRequest(); + void closeServerSocket(); + + bool isBonded() const; + QVector<TempClientConfigurationData> gatherClientConfigData(); + void storeClientConfigurations(); + void restoreClientConfigurations(); - void sendCommand(const QByteArray &packet); + void sendPacket(const QByteArray &packet); void sendNextPendingRequest(); void processReply(const Request &request, const QByteArray &reply); @@ -212,6 +297,73 @@ private: void resetController(); + void handleAdvertisingError(); + + bool checkPacketSize(const QByteArray &packet, int minSize, int maxSize = -1); + bool checkHandle(const QByteArray &packet, QLowEnergyHandle handle); + bool checkHandlePair(quint8 request, QLowEnergyHandle startingHandle, + QLowEnergyHandle endingHandle); + + void handleExchangeMtuRequest(const QByteArray &packet); + void handleFindInformationRequest(const QByteArray &packet); + void handleFindByTypeValueRequest(const QByteArray &packet); + void handleReadByTypeRequest(const QByteArray &packet); + void handleReadRequest(const QByteArray &packet); + void handleReadBlobRequest(const QByteArray &packet); + void handleReadMultipleRequest(const QByteArray &packet); + void handleReadByGroupTypeRequest(const QByteArray &packet); + void handleWriteRequestOrCommand(const QByteArray &packet); + void handlePrepareWriteRequest(const QByteArray &packet); + void handleExecuteWriteRequest(const QByteArray &packet); + + void sendErrorResponse(quint8 request, quint16 handle, quint8 code); + + using ElemWriter = std::function<void(const Attribute &, char *&)>; + 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); + + using AttributePredicate = std::function<bool(const Attribute &)>; + QVector<Attribute> getAttributes(QLowEnergyHandle startHandle, QLowEnergyHandle endHandle, + const AttributePredicate &attributePredicate = [](const Attribute &) { return true; }); + + int checkPermissions(const Attribute &attr, QLowEnergyCharacteristic::PropertyType type); + 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(); @@ -245,6 +397,8 @@ private: }; +Q_DECLARE_TYPEINFO(QLowEnergyControllerPrivate::Attribute, Q_MOVABLE_TYPE); + QT_END_NAMESPACE #endif // QT_OSX_BLUETOOTH || QT_IOS_BLUETOOTH diff --git a/src/bluetooth/qlowenergydescriptordata.cpp b/src/bluetooth/qlowenergydescriptordata.cpp new file mode 100644 index 00000000..db5ea238 --- /dev/null +++ b/src/bluetooth/qlowenergydescriptordata.cpp @@ -0,0 +1,219 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergydescriptordata.h" + +#include <QtCore/qbytearray.h> + +QT_BEGIN_NAMESPACE + +struct QLowEnergyDescriptorDataPrivate : public QSharedData +{ + QLowEnergyDescriptorDataPrivate() : readable(true), writable(true) {} + + QBluetoothUuid uuid; + QByteArray value; + QBluetooth::AttAccessConstraints readConstraints; + QBluetooth::AttAccessConstraints writeConstraints; + bool readable; + bool writable; +}; + +/*! + \since 5.7 + \class QLowEnergyDescriptorData + \brief The QLowEnergyDescriptorData class is used to create GATT service data. + \inmodule QtBluetooth + \ingroup shared + + An object of this class provides a descriptor to be added to a + \l QLowEnergyCharacteristicData object via \l QLowEnergyCharacteristicData::addDescriptor(). + + \note The member functions related to access permissions are only applicable to those + types of descriptors for which the Bluetooth specification does not prescribe if + and how their values can be accessed. + + \sa QLowEnergyCharacteristicData + \sa QLowEnergyServiceData + \sa QLowEnergyController::addService +*/ + +/*! Creates a new invalid object of this class. */ +QLowEnergyDescriptorData::QLowEnergyDescriptorData() : d(new QLowEnergyDescriptorDataPrivate) +{ +} + +/*! + Creates a new object of this class with UUID and value being provided by \a uuid and \a value, + respectively. + */ +QLowEnergyDescriptorData::QLowEnergyDescriptorData(const QBluetoothUuid &uuid, + const QByteArray &value) + : d(new QLowEnergyDescriptorDataPrivate) +{ + setUuid(uuid); + setValue(value); +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyDescriptorData::QLowEnergyDescriptorData(const QLowEnergyDescriptorData &other) + : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyDescriptorData::~QLowEnergyDescriptorData() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyDescriptorData &QLowEnergyDescriptorData::operator=(const QLowEnergyDescriptorData &other) +{ + d = other.d; + return *this; +} + +/*! Returns the value of this descriptor. */ +QByteArray QLowEnergyDescriptorData::value() const +{ + return d->value; +} + +/*! + Sets the value of this descriptor. It will be sent to a peer device exactly the way it is + provided here, so callers need to take care of things such as endianness. + */ +void QLowEnergyDescriptorData::setValue(const QByteArray &value) +{ + d->value = value; +} + +/*! Returns the UUID of this descriptor. */ +QBluetoothUuid QLowEnergyDescriptorData::uuid() const +{ + return d->uuid; +} + +/*! Sets the UUID of this descriptor to \a uuid. */ +void QLowEnergyDescriptorData::setUuid(const QBluetoothUuid &uuid) +{ + d->uuid = uuid; +} + +/*! Returns true if and only if this object has a non-null UUID. */ +bool QLowEnergyDescriptorData::isValid() const +{ + return !uuid().isNull(); +} + +/*! + Specifies whether the value of this descriptor is \a readable and if so, under which + \a constraints. + \sa setWritePermissions() + */ +void QLowEnergyDescriptorData::setReadPermissions(bool readable, + QBluetooth::AttAccessConstraints constraints) +{ + d->readable = readable; + d->readConstraints = constraints; +} + +/*! Returns \c true if the value of this descriptor is readable and \c false otherwise. */ +bool QLowEnergyDescriptorData::isReadable() const +{ + return d->readable; +} + +/*! + Returns the constraints under which the value of this descriptor can be read. This value + is only relevant if \l isReadable() returns \c true. + */ +QBluetooth::AttAccessConstraints QLowEnergyDescriptorData::readConstraints() const +{ + return d->readConstraints; +} + +/*! + Specifies whether the value of this descriptor is \a writable and if so, under which + \a constraints. + \sa setReadPermissions() + */ +void QLowEnergyDescriptorData::setWritePermissions(bool writable, + QBluetooth::AttAccessConstraints constraints) +{ + d->writable = writable; + d->writeConstraints = constraints; +} + +/*! Returns \c true if the value of this descriptor is writable and \c false otherwise. */ +bool QLowEnergyDescriptorData::isWritable() const +{ + return d->writable; +} + +/*! + Returns the constraints under which the value of this descriptor can be written. This value + is only relevant if \l isWritable() returns \c true. + */ +QBluetooth::AttAccessConstraints QLowEnergyDescriptorData::writeConstraints() const +{ + return d->writeConstraints; +} + +/*! + \fn void QLowEnergyDescriptorData::swap(QLowEnergyDescriptorData &other) + Swaps this object with \a other. + */ + +/*! + Returns \c true if \a d1 and \a d2 are equal with respect to their public state, + otherwise returns \c false. + */ +bool operator==(const QLowEnergyDescriptorData &d1, const QLowEnergyDescriptorData &d2) +{ + return d1.d == d2.d || ( + d1.uuid() == d2.uuid() + && d1.value() == d2.value() + && d1.isReadable() == d2.isReadable() + && d1.isWritable() == d2.isWritable() + && d1.readConstraints() == d2.readConstraints() + && d1.writeConstraints() == d2.writeConstraints()); +} + +/*! + \fn bool operator!=(const QLowEnergyDescriptorData &d1, const QLowEnergyDescriptorData &d2) + Returns \c true if \a d1 and \a d2 are not equal with respect to their public state, + otherwise returns \c false. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergydescriptordata.h b/src/bluetooth/qlowenergydescriptordata.h new file mode 100644 index 00000000..c46eb249 --- /dev/null +++ b/src/bluetooth/qlowenergydescriptordata.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOWENERGYDESCRIPTORDATA_H +#define QLOWENERGYDESCRIPTORDATA_H + +#include <QtBluetooth/qbluetooth.h> +#include <QtBluetooth/qbluetoothuuid.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QByteArray; +struct QLowEnergyDescriptorDataPrivate; + +class Q_BLUETOOTH_EXPORT QLowEnergyDescriptorData +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyDescriptorData &d1, + const QLowEnergyDescriptorData &d12); +public: + QLowEnergyDescriptorData(); + QLowEnergyDescriptorData(const QBluetoothUuid &uuid, + const QByteArray &value); + QLowEnergyDescriptorData(const QLowEnergyDescriptorData &other); + ~QLowEnergyDescriptorData(); + + QLowEnergyDescriptorData &operator=(const QLowEnergyDescriptorData &other); + + QByteArray value() const; + void setValue(const QByteArray &value); + + QBluetoothUuid uuid() const; + void setUuid(const QBluetoothUuid &uuid); + + bool isValid() const; + + void setReadPermissions(bool readable, + QBluetooth::AttAccessConstraints constraints = QBluetooth::AttAccessConstraints()); + bool isReadable() const; + QBluetooth::AttAccessConstraints readConstraints() const; + + void setWritePermissions(bool writable, + QBluetooth::AttAccessConstraints constraints = QBluetooth::AttAccessConstraints()); + bool isWritable() const; + QBluetooth::AttAccessConstraints writeConstraints() const; + + void swap(QLowEnergyDescriptorData &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + +private: + QSharedDataPointer<QLowEnergyDescriptorDataPrivate> d; +}; + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyDescriptorData &d1, + const QLowEnergyDescriptorData &d2); + +inline bool operator!=(const QLowEnergyDescriptorData &d1, const QLowEnergyDescriptorData &d2) +{ + return !(d1 == d2); +} + +Q_DECLARE_SHARED(QLowEnergyDescriptorData) + +QT_END_NAMESPACE + +#endif // Include guard. diff --git a/src/bluetooth/qlowenergyservice.cpp b/src/bluetooth/qlowenergyservice.cpp index 6d32fb71..c9f21c70 100644 --- a/src/bluetooth/qlowenergyservice.cpp +++ b/src/bluetooth/qlowenergyservice.cpp @@ -226,6 +226,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. */ /*! @@ -308,15 +311,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. + */ /*! @@ -335,8 +343,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() */ @@ -617,7 +627,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. @@ -646,6 +662,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() */ @@ -656,7 +687,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; } @@ -733,9 +767,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. @@ -747,6 +786,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, @@ -754,7 +798,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 49960014..496c9dde 100644 --- a/src/bluetooth/qlowenergyservice.h +++ b/src/bluetooth/qlowenergyservice.h @@ -75,7 +75,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) @@ -132,6 +133,7 @@ private: // QLowEnergyController is the factory for this class friend class QLowEnergyController; + friend class QLowEnergyControllerPrivate; QLowEnergyService(QSharedPointer<QLowEnergyServicePrivate> p, QObject *parent = 0); }; diff --git a/src/bluetooth/qlowenergyservicedata.cpp b/src/bluetooth/qlowenergyservicedata.cpp new file mode 100644 index 00000000..539f8472 --- /dev/null +++ b/src/bluetooth/qlowenergyservicedata.cpp @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergyservicedata.h" + +#include "qlowenergycharacteristicdata.h" + +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QT_BT) + +struct QLowEnergyServiceDataPrivate : public QSharedData +{ + QLowEnergyServiceDataPrivate() : type(QLowEnergyServiceData::ServiceTypePrimary) {} + + QLowEnergyServiceData::ServiceType type; + QBluetoothUuid uuid; + QList<QLowEnergyService *> includedServices; + QList<QLowEnergyCharacteristicData> characteristics; +}; + + +/*! + \since 5.7 + \class QLowEnergyServiceData + \brief The QLowEnergyServiceData class is used to set up GATT service data. + \inmodule QtBluetooth + \ingroup shared + + An Object of this class provides a service to be added to a GATT server via + \l QLowEnergyController::addService(). +*/ + +/*! + \enum QLowEnergyServiceData::ServiceType + The type of GATT service. + + \value ServiceTypePrimary + The service is a primary service. + \value ServiceTypeSecondary + The service is a secondary service. Secondary services are included by other services + to implement some higher-level functionality. + */ + +/*! Creates a new invalid object of this class. */ +QLowEnergyServiceData::QLowEnergyServiceData() : d(new QLowEnergyServiceDataPrivate) +{ +} + +/*! Constructs a new object of this class that is a copy of \a other. */ +QLowEnergyServiceData::QLowEnergyServiceData(const QLowEnergyServiceData &other) : d(other.d) +{ +} + +/*! Destroys this object. */ +QLowEnergyServiceData::~QLowEnergyServiceData() +{ +} + +/*! Makes this object a copy of \a other and returns the new value of this object. */ +QLowEnergyServiceData &QLowEnergyServiceData::operator=(const QLowEnergyServiceData &other) +{ + d = other.d; + return *this; +} + +/*! Returns the type of this service. */ +QLowEnergyServiceData::ServiceType QLowEnergyServiceData::type() const +{ + return d->type; +} + +/*! Sets the type of this service to \a type. */ +void QLowEnergyServiceData::setType(ServiceType type) +{ + d->type = type; +} + +/*! Returns the UUID of this service. */ +QBluetoothUuid QLowEnergyServiceData::uuid() const +{ + return d->uuid; +} + +/*! Sets the UUID of this service to \a uuid. */ +void QLowEnergyServiceData::setUuid(const QBluetoothUuid &uuid) +{ + d->uuid = uuid; +} + +/*! Returns the list of included services. */ +QList<QLowEnergyService *> QLowEnergyServiceData::includedServices() const +{ + return d->includedServices; +} + +/*! + Sets the list of included services to \a services. + All objects in this list must have been returned from a call to + \l QLowEnergyController::addService. + \sa addIncludedService() +*/ +void QLowEnergyServiceData::setIncludedServices(const QList<QLowEnergyService *> &services) +{ + d->includedServices = services; +} + +/*! + Adds \a service to the list of included services. + The \a service object must have been returned from a call to + \l QLowEnergyController::addService. + \sa setIncludedServices() +*/ +void QLowEnergyServiceData::addIncludedService(QLowEnergyService *service) +{ + d->includedServices << service; +} + +/*! Returns the list of characteristics. */ +QList<QLowEnergyCharacteristicData> QLowEnergyServiceData::characteristics() const +{ + return d->characteristics; +} + +/*! + Sets the list of characteristics to \a characteristics. + Only valid characteristics are considered. + \sa addCharacteristic() + */ +void QLowEnergyServiceData::setCharacteristics(const QList<QLowEnergyCharacteristicData> &characteristics) +{ + foreach (const QLowEnergyCharacteristicData &cd, characteristics) + addCharacteristic(cd); +} + +/*! + Adds \a characteristic to the list of characteristics, if it is valid. + \sa setCharacteristics() + */ +void QLowEnergyServiceData::addCharacteristic(const QLowEnergyCharacteristicData &characteristic) +{ + if (characteristic.isValid()) + d->characteristics << characteristic; + else + qCWarning(QT_BT) << "not adding invalid characteristic to service"; +} + +/*! Returns \c true if this service is has a non-null UUID. */ +bool QLowEnergyServiceData::isValid() const +{ + return !uuid().isNull(); +} + +/*! + \fn void QLowEnergyServiceData::swap(QLowEnergyServiceData &other) + Swaps this object with \a other. + */ + +/*! + Returns \c true if \a sd1 and \a sd2 are equal with respect to their public state, + otherwise returns \c false. + */ +bool operator==(const QLowEnergyServiceData sd1, const QLowEnergyServiceData &sd2) +{ + return sd1.d == sd2.d || (sd1.type() == sd2.type() && sd1.uuid() == sd2.uuid() + && sd1.includedServices() == sd2.includedServices() + && sd1.characteristics() == sd2.characteristics()); +} + +/*! + \fn bool operator!=(const QLowEnergyServiceData &sd1, + const QLowEnergyServiceData &sd2) + Returns \c true if \a sd1 and \a sd2 are not equal with respect to their public state, + otherwise returns \c false. + */ + +QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergyservicedata.h b/src/bluetooth/qlowenergyservicedata.h new file mode 100644 index 00000000..c60dd8ee --- /dev/null +++ b/src/bluetooth/qlowenergyservicedata.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOWENERGYSERVICEDATA_H +#define QLOWENERGYSERVICEDATA_H + +#include <QtBluetooth/qbluetoothglobal.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothUuid; +class QLowEnergyCharacteristicData; +class QLowEnergyService; +struct QLowEnergyServiceDataPrivate; + +class Q_BLUETOOTH_EXPORT QLowEnergyServiceData +{ + friend Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyServiceData sd1, + const QLowEnergyServiceData &sd2); +public: + QLowEnergyServiceData(); + QLowEnergyServiceData(const QLowEnergyServiceData &other); + ~QLowEnergyServiceData(); + + QLowEnergyServiceData &operator=(const QLowEnergyServiceData &other); + + enum ServiceType { ServiceTypePrimary = 0x2800, ServiceTypeSecondary = 0x2801 }; + ServiceType type() const; + void setType(ServiceType type); + + QBluetoothUuid uuid() const; + void setUuid(const QBluetoothUuid &uuid); + + QList<QLowEnergyService *> includedServices() const; + void setIncludedServices(const QList<QLowEnergyService *> &services); + void addIncludedService(QLowEnergyService *service); + + QList<QLowEnergyCharacteristicData> characteristics() const; + void setCharacteristics(const QList<QLowEnergyCharacteristicData> &characteristics); + void addCharacteristic(const QLowEnergyCharacteristicData &characteristic); + + bool isValid() const; + + void swap(QLowEnergyServiceData &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + +private: + QSharedDataPointer<QLowEnergyServiceDataPrivate> d; +}; + +Q_BLUETOOTH_EXPORT bool operator==(const QLowEnergyServiceData sd1, + const QLowEnergyServiceData &sd2); +inline bool operator!=(const QLowEnergyServiceData sd1, const QLowEnergyServiceData &sd2) +{ + return !(sd1 == sd2); +} + +Q_DECLARE_SHARED(QLowEnergyServiceData) + +QT_END_NAMESPACE + +#endif // Include guard. diff --git a/src/bluetooth/qlowenergyserviceprivate.cpp b/src/bluetooth/qlowenergyserviceprivate.cpp index d109fdb1..83724d4e 100644 --- a/src/bluetooth/qlowenergyserviceprivate.cpp +++ b/src/bluetooth/qlowenergyserviceprivate.cpp @@ -39,6 +39,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 a18273d7..bde099ba 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -57,10 +57,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/auto.pro b/tests/auto/auto.pro index 9bbf792d..f1112240 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -20,6 +20,7 @@ qtHaveModule(bluetooth) { qlowenergycharacteristic \ qlowenergydescriptor \ qlowenergycontroller \ + qlowenergycontroller-gattserver \ qlowenergyservice } diff --git a/tests/auto/qlowenergycontroller-gattserver/README b/tests/auto/qlowenergycontroller-gattserver/README new file mode 100644 index 00000000..bd63ef85 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/README @@ -0,0 +1,10 @@ +This test is split into a server and a client part. The former is supplying data, and +the latter is implementing the actual test application. +To run a full test, follow these steps: + 1) Start the server application on some machine that has a Bluetooth LE adapter + and is close enough to the client machine. + 2) On the client machine, set the QT_BT_GATTSERVER_TEST_ADDRESS environment variable + to the address of the Bluetooth adapter on the server machine. + 3) Run the test on the client. +If you skip steps 1) or 2), only a few unit tests will be run. These do not require the +test machine to have a Bluetooth adapter. diff --git a/tests/auto/qlowenergycontroller-gattserver/qlowenergycontroller-gattserver.pro b/tests/auto/qlowenergycontroller-gattserver/qlowenergycontroller-gattserver.pro new file mode 100644 index 00000000..8b6c52e7 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/qlowenergycontroller-gattserver.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS = server test diff --git a/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp new file mode 100644 index 00000000..087b0284 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp @@ -0,0 +1,226 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtBluetooth/qlowenergyadvertisingdata.h> +#include <QtBluetooth/qlowenergyadvertisingparameters.h> +#include <QtBluetooth/qlowenergyconnectionparameters.h> +#include <QtBluetooth/qlowenergycontroller.h> +#include <QtBluetooth/qlowenergycharacteristicdata.h> +#include <QtBluetooth/qlowenergydescriptordata.h> +#include <QtBluetooth/qlowenergyservicedata.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qendian.h> +#include <QtCore/qhash.h> +#include <QtCore/qscopedpointer.h> +#include <QtCore/qsharedpointer.h> +#include <QtCore/qvector.h> + +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) +{ + const ServicePtr service(leController->addService(serviceData)); + Q_ASSERT(service); + services.insert(service->serviceUuid(), service); +} + +void addRunningSpeedService() +{ + QLowEnergyServiceData serviceData; + serviceData.setUuid(QBluetoothUuid::RunningSpeedAndCadence); + serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); + + QLowEnergyDescriptorData desc; + desc.setUuid(QBluetoothUuid::ClientCharacteristicConfiguration); + desc.setValue(QByteArray(2, 0)); // Default: No indication, no notification. + QLowEnergyCharacteristicData charData; + charData.setUuid(QBluetoothUuid::RSCMeasurement); + charData.addDescriptor(desc); + charData.setProperties(QLowEnergyCharacteristic::Notify); + QByteArray value(4, 0); + value[0] = 1 << 2; // "Running", no optional fields. + charData.setValue(value); + serviceData.addCharacteristic(charData); + + charData = QLowEnergyCharacteristicData(); + charData.setUuid(QBluetoothUuid::RSCFeature); + charData.setProperties(QLowEnergyCharacteristic::Read); + value = QByteArray(2, 0); + qToLittleEndian<quint16>(1 << 2, reinterpret_cast<uchar *>(value.data())); + charData.setValue(value); + serviceData.addCharacteristic(charData); + addService(serviceData); +} + +void addGenericAccessService() +{ + QLowEnergyServiceData serviceData; + serviceData.setUuid(QBluetoothUuid::GenericAccess); + serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); + + QLowEnergyCharacteristicData charData; + charData.setUuid(QBluetoothUuid::DeviceName); + charData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write); + charData.setValue(deviceName()); + serviceData.addCharacteristic(charData); + + charData = QLowEnergyCharacteristicData(); + charData.setUuid(QBluetoothUuid::Appearance); + charData.setProperties(QLowEnergyCharacteristic::Read); + QByteArray value(2, 0); + qToLittleEndian<quint16>(128, reinterpret_cast<uchar *>(value.data())); // Generic computer. + charData.setValue(value); + serviceData.addCharacteristic(charData); + + serviceData.addIncludedService(services.value(QBluetoothUuid::RunningSpeedAndCadence).data()); + addService(serviceData); +} + +void addCustomService() +{ + QLowEnergyServiceData serviceData; + serviceData.setUuid(QBluetoothUuid(quint16(0x2000))); + serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); + + QLowEnergyCharacteristicData charData; + charData.setUuid(QBluetoothUuid(quint16(0x5000))); // Made up. + charData.setProperties(QLowEnergyCharacteristic::Read); + charData.setValue(QByteArray(1024, 'x')); // Long value to test "Read Blob". + serviceData.addCharacteristic(charData); + + 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); +} + +void startAdvertising() +{ + QLowEnergyAdvertisingParameters params; + params.setMode(QLowEnergyAdvertisingParameters::AdvInd); + QLowEnergyAdvertisingData data; + data.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityLimited); + data.setServices(services.keys()); + data.setIncludePowerLevel(true); + data.setLocalName(deviceName()); + leController->startAdvertising(params, data); +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + leController.reset(QLowEnergyController::createPeripheral()); + addRunningSpeedService(); + addGenericAccessService(); + addCustomService(); + startAdvertising(); + + 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"); + QLowEnergyConnectionParameters connParams; + connParams.setIntervalRange(30, 62.5); + connParams.setLatency(5); + connParams.setSupervisionTimeout(5500); + leController->requestConnectionUpdate(connParams); + }; + QObject::connect(customService.data(), &QLowEnergyService::descriptorWritten, + descriptorWriteHandler); + + return app.exec(); +} diff --git a/tests/auto/qlowenergycontroller-gattserver/server/server.pro b/tests/auto/qlowenergycontroller-gattserver/server/server.pro new file mode 100644 index 00000000..b9f2ccf9 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/server/server.pro @@ -0,0 +1,5 @@ +QT = core bluetooth + +CONFIG += c++11 + +SOURCES = qlowenergycontroller-gattserver.cpp diff --git a/tests/auto/qlowenergycontroller-gattserver/test/test.pro b/tests/auto/qlowenergycontroller-gattserver/test/test.pro new file mode 100644 index 00000000..bd1f0874 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/test/test.pro @@ -0,0 +1,6 @@ +QT = core bluetooth testlib + +TARGET = tst_qlowenergycontroller-gattserver +CONFIG += testcase c++11 + +SOURCES += tst_qlowenergycontroller-gattserver.cpp diff --git a/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp new file mode 100644 index 00000000..e2534a14 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp @@ -0,0 +1,519 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#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/qlowenergyconnectionparameters.h> +#include <QtBluetooth/qlowenergycontroller.h> +#include <QtBluetooth/qlowenergycharacteristicdata.h> +#include <QtBluetooth/qlowenergydescriptordata.h> +#include <QtBluetooth/qlowenergyservicedata.h> +#include <QtCore/qendian.h> +#include <QtCore/qscopedpointer.h> +#include <QtTest/qsignalspy.h> +#include <QtTest/QtTest> + +#include <algorithm> + +using namespace QBluetooth; + +class TestQLowEnergyControllerGattServer : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + // Static, local stuff goes here. + void advertisingParameters(); + void advertisingData(); + void connectionParameters(); + void controllerType(); + void serviceData(); + + // Interaction with actual GATT server goes here. Order is relevant. + void advertisedData(); + void serverCommunication(); + +private: + QBluetoothAddress m_serverAddress; + QBluetoothDeviceInfo m_serverInfo; + QScopedPointer<QLowEnergyController> m_leController; +}; + + +void TestQLowEnergyControllerGattServer::initTestCase() +{ + const QString serverAddress = qgetenv("QT_BT_GATTSERVER_TEST_ADDRESS"); + if (serverAddress.isEmpty()) + return; + m_serverAddress = QBluetoothAddress(serverAddress); + QVERIFY(!m_serverAddress.isNull()); +} + +void TestQLowEnergyControllerGattServer::advertisingParameters() +{ + QLowEnergyAdvertisingParameters params; + QCOMPARE(params, QLowEnergyAdvertisingParameters()); + QCOMPARE(params.filterPolicy(), QLowEnergyAdvertisingParameters::IgnoreWhiteList); + QCOMPARE(params.minimumInterval(), 1280); + QCOMPARE(params.maximumInterval(), 1280); + QCOMPARE(params.mode(), QLowEnergyAdvertisingParameters::AdvInd); + QVERIFY(params.whiteList().isEmpty()); + + params.setInterval(100, 200); + QCOMPARE(params.minimumInterval(), 100); + QCOMPARE(params.maximumInterval(), 200); + params.setInterval(200, 100); + QCOMPARE(params.minimumInterval(), 200); + QCOMPARE(params.maximumInterval(), 200); + + params.setMode(QLowEnergyAdvertisingParameters::AdvScanInd); + QCOMPARE(params.mode(), QLowEnergyAdvertisingParameters::AdvScanInd); + + const auto whiteList = QList<QLowEnergyAdvertisingParameters::AddressInfo>() + << QLowEnergyAdvertisingParameters::AddressInfo(QBluetoothAddress(), + QLowEnergyController::PublicAddress); + params.setWhiteList(whiteList, QLowEnergyAdvertisingParameters::UseWhiteListForConnecting); + QCOMPARE(params.whiteList(), whiteList); + QCOMPARE(params.filterPolicy(), QLowEnergyAdvertisingParameters::UseWhiteListForConnecting); + QVERIFY(params != QLowEnergyAdvertisingParameters()); +} + +void TestQLowEnergyControllerGattServer::advertisingData() +{ + QLowEnergyAdvertisingData data; + QCOMPARE(data, QLowEnergyAdvertisingData()); + QCOMPARE(data.discoverability(), QLowEnergyAdvertisingData::DiscoverabilityNone); + QCOMPARE(data.includePowerLevel(), false); + QCOMPARE(data.localName(), QString()); + QCOMPARE(data.manufacturerData(), QByteArray()); + QCOMPARE(data.manufacturerId(), QLowEnergyAdvertisingData::invalidManufacturerId()); + QVERIFY(data.services().isEmpty()); + + data.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityLimited); + QCOMPARE(data.discoverability(), QLowEnergyAdvertisingData::DiscoverabilityLimited); + + data.setIncludePowerLevel(true); + QCOMPARE(data.includePowerLevel(), true); + + data.setLocalName("A device name"); + QCOMPARE(data.localName(), QString("A device name")); + + data.setManufacturerData(0xfffd, "some data"); + QCOMPARE(data.manufacturerId(), quint16(0xfffd)); + QCOMPARE(data.manufacturerData(), QByteArray("some data")); + + const auto services = QList<QBluetoothUuid>() << QBluetoothUuid::CurrentTimeService + << QBluetoothUuid::DeviceInformation; + data.setServices(services); + QCOMPARE(data.services(), services); + + QByteArray rawData(7, 'x'); + data.setRawData(rawData); + QCOMPARE(data.rawData(), rawData); + + QVERIFY(data != QLowEnergyAdvertisingData()); +} + +void TestQLowEnergyControllerGattServer::connectionParameters() +{ + QLowEnergyConnectionParameters connParams; + QCOMPARE(connParams, QLowEnergyConnectionParameters()); + connParams.setIntervalRange(8, 9); + QCOMPARE(connParams.minimumInterval(), double(8)); + QCOMPARE(connParams.maximumInterval(), double(9)); + connParams.setIntervalRange(9, 8); + QCOMPARE(connParams.minimumInterval(), double(9)); + QCOMPARE(connParams.maximumInterval(), double(9)); + connParams.setLatency(50); + QCOMPARE(connParams.latency(), 50); + connParams.setSupervisionTimeout(1000); + QCOMPARE(connParams.supervisionTimeout(), 1000); + const QLowEnergyConnectionParameters cp2 = connParams; + QCOMPARE(cp2, connParams); + QLowEnergyConnectionParameters cp3; + QVERIFY(cp3 != connParams); + cp3 = connParams; + QCOMPARE(cp3, connParams); +} + +void TestQLowEnergyControllerGattServer::advertisedData() +{ + if (m_serverAddress.isNull()) + QSKIP("No server address provided"); + QBluetoothDeviceDiscoveryAgent discoveryAgent; + discoveryAgent.start(); + QSignalSpy spy(&discoveryAgent, SIGNAL(finished())); + QVERIFY(spy.wait(30000)); + const QList<QBluetoothDeviceInfo> devices = discoveryAgent.discoveredDevices(); + const auto it = std::find_if(devices.constBegin(), devices.constEnd(), + [this](const QBluetoothDeviceInfo &device) { return device.address() == m_serverAddress; }); + QVERIFY(it != devices.constEnd()); + m_serverInfo = *it; + + // BlueZ seems to interfere with the advertising in some way, so that in addition to the name + // we set, the host name of the machine is also sent. Therefore we cannot guarantee that "our" + // name is seen on the scanning machine. + // QCOMPARE(m_serverInfo.name(), QString("Qt GATT server")); + + QCOMPARE(m_serverInfo.serviceUuids().count(), 3); + QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid::GenericAccess)); + QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid::RunningSpeedAndCadence)); + QVERIFY(m_serverInfo.serviceUuids().contains(QBluetoothUuid(quint16(0x2000)))); +} + +// 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)); + QVERIFY(!m_leController.isNull()); + m_leController->connectToDevice(); + QScopedPointer<QSignalSpy> spy(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)); + const QList<QBluetoothUuid> serviceUuids = m_leController->services(); + QCOMPARE(serviceUuids.count(), 3); + QVERIFY(serviceUuids.contains(QBluetoothUuid::GenericAccess)); + QVERIFY(serviceUuids.contains(QBluetoothUuid::RunningSpeedAndCadence)); + QVERIFY(serviceUuids.contains(QBluetoothUuid(quint16(0x2000)))); + + const QScopedPointer<QLowEnergyService> genericAccessService( + m_leController->createServiceObject(QBluetoothUuid::GenericAccess)); + QVERIFY(!genericAccessService.isNull()); + genericAccessService->discoverDetails(); + while (genericAccessService->state() != QLowEnergyService::ServiceDiscovered) { + spy.reset(new QSignalSpy(genericAccessService.data(), &QLowEnergyService::stateChanged)); + QVERIFY(spy->wait(3000)); + } + QCOMPARE(genericAccessService->includedServices().count(), 1); + QCOMPARE(genericAccessService->includedServices().first(), + QBluetoothUuid(QBluetoothUuid::RunningSpeedAndCadence)); + QCOMPARE(genericAccessService->characteristics().count(), 2); + const QLowEnergyCharacteristic deviceNameChar + = genericAccessService->characteristic(QBluetoothUuid::DeviceName); + QVERIFY(deviceNameChar.isValid()); + QCOMPARE(deviceNameChar.descriptors().count(), 0); + QCOMPARE(deviceNameChar.properties(), + QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write); + QCOMPARE(deviceNameChar.value().constData(), "Qt GATT server"); + const QLowEnergyCharacteristic appearanceChar + = genericAccessService->characteristic(QBluetoothUuid::Appearance); + QVERIFY(appearanceChar.isValid()); + QCOMPARE(appearanceChar.descriptors().count(), 0); + QCOMPARE(appearanceChar.properties(), QLowEnergyCharacteristic::Read); + auto value = qFromLittleEndian<quint16>(reinterpret_cast<const uchar *>( + appearanceChar.value().constData())); + QCOMPARE(value, quint16(128)); + + const QScopedPointer<QLowEnergyService> runningSpeedService( + m_leController->createServiceObject(QBluetoothUuid::RunningSpeedAndCadence)); + QVERIFY(!runningSpeedService.isNull()); + runningSpeedService->discoverDetails(); + while (runningSpeedService->state() != QLowEnergyService::ServiceDiscovered) { + spy.reset(new QSignalSpy(runningSpeedService.data(), &QLowEnergyService::stateChanged)); + QVERIFY(spy->wait(3000)); + } + QCOMPARE(runningSpeedService->includedServices().count(), 0); + QCOMPARE(runningSpeedService->characteristics().count(), 2); + QLowEnergyCharacteristic measurementChar + = runningSpeedService->characteristic(QBluetoothUuid::RSCMeasurement); + QVERIFY(measurementChar.isValid()); + QCOMPARE(measurementChar.descriptors().count(), 1); + const QLowEnergyDescriptor clientConfigDesc + = measurementChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + QVERIFY(clientConfigDesc.isValid()); + QCOMPARE(clientConfigDesc.value(), QByteArray(2, 0)); + QCOMPARE(measurementChar.properties(), QLowEnergyCharacteristic::Notify); + QCOMPARE(measurementChar.value(), QByteArray()); // Empty because Read property not set + QLowEnergyCharacteristic featureChar + = runningSpeedService->characteristic(QBluetoothUuid::RSCFeature); + QVERIFY(featureChar.isValid()); + QCOMPARE(featureChar.descriptors().count(), 0); + QCOMPARE(featureChar.properties(), QLowEnergyCharacteristic::Read); + value = qFromLittleEndian<quint16>(reinterpret_cast<const uchar *>( + featureChar.value().constData())); + QCOMPARE(value, quint16(1 << 2)); + + 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(5000)); + } + QCOMPARE(customService->includedServices().count(), 0); + QCOMPARE(customService->characteristics().count(), 4); + QLowEnergyCharacteristic customChar + = customService->characteristic(QBluetoothUuid(quint16(0x5000))); + QVERIFY(customChar.isValid()); + QCOMPARE(customChar.descriptors().count(), 0); + QCOMPARE(customChar.value(), QByteArray(1024, 'x')); + + QLowEnergyCharacteristic customChar2 + = customService->characteristic(QBluetoothUuid(quint16(0x5001))); + QVERIFY(customChar2.isValid()); + 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); + qToLittleEndian<quint16>(2, reinterpret_cast<uchar *>(indicateValue.data())); + customService->writeDescriptor(cc3ClientConfig, indicateValue); + spy.reset(new QSignalSpy(customService.data(), &QLowEnergyService::descriptorWritten)); + QVERIFY(spy->wait(3000)); + + QByteArray notifyValue(2, 0); + qToLittleEndian<quint16>(1, reinterpret_cast<uchar *>(notifyValue.data())); + 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"); + + spy.reset(new QSignalSpy(m_leController.data(), &QLowEnergyController::connectionUpdated)); + QVERIFY(spy->wait(5000)); + + 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() +{ + const QScopedPointer<QLowEnergyController> controller(QLowEnergyController::createPeripheral()); + QVERIFY(!controller.isNull()); + QCOMPARE(controller->role(), QLowEnergyController::PeripheralRole); +} + +void TestQLowEnergyControllerGattServer::serviceData() +{ + QLowEnergyDescriptorData descData; + QVERIFY(!descData.isValid()); + + descData.setUuid(QBluetoothUuid::ValidRange); + QCOMPARE(descData.uuid(), QBluetoothUuid(QBluetoothUuid::ValidRange)); + QVERIFY(descData.isValid()); + QVERIFY(descData != QLowEnergyDescriptorData()); + + descData.setValue("xyz"); + QCOMPARE(descData.value().constData(), "xyz"); + + descData.setReadPermissions(true, AttAuthenticationRequired); + QCOMPARE(descData.isReadable(), true); + QCOMPARE(descData.readConstraints(), AttAuthenticationRequired); + + descData.setWritePermissions(false); + QCOMPARE(descData.isWritable(), false); + + QLowEnergyDescriptorData descData2(QBluetoothUuid::ReportReference, "abc"); + QVERIFY(descData2 != QLowEnergyDescriptorData()); + QVERIFY(descData2.isValid()); + QCOMPARE(descData2.uuid(), QBluetoothUuid(QBluetoothUuid::ReportReference)); + QCOMPARE(descData2.value().constData(), "abc"); + + QLowEnergyCharacteristicData charData; + QVERIFY(!charData.isValid()); + + charData.setUuid(QBluetoothUuid::BatteryLevel); + QVERIFY(charData != QLowEnergyCharacteristicData()); + QCOMPARE(charData.uuid(), QBluetoothUuid(QBluetoothUuid::BatteryLevel)); + QVERIFY(charData.isValid()); + + 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); + QCOMPARE(charData.properties(), props); + + charData.setReadConstraints(AttEncryptionRequired); + QCOMPARE(charData.readConstraints(), AttEncryptionRequired); + charData.setWriteConstraints(AttAuthenticationRequired | AttAuthorizationRequired); + QCOMPARE(charData.writeConstraints(), AttAuthenticationRequired | AttAuthorizationRequired); + + charData.setDescriptors(QList<QLowEnergyDescriptorData>() << descData << descData2); + QLowEnergyDescriptorData descData3(QBluetoothUuid::ExternalReportReference, "someval"); + charData.addDescriptor(descData3); + charData.addDescriptor(QLowEnergyDescriptorData()); // Invalid. + QCOMPARE(charData.descriptors(), + QList<QLowEnergyDescriptorData>() << descData << descData2 << descData3); + + QLowEnergyServiceData secondaryData; + QVERIFY(!secondaryData.isValid()); + + secondaryData.setUuid(QBluetoothUuid::SerialPort); + QCOMPARE(secondaryData.uuid(), QBluetoothUuid(QBluetoothUuid::SerialPort)); + QVERIFY(secondaryData.isValid()); + QVERIFY(secondaryData != QLowEnergyServiceData()); + + secondaryData.setType(QLowEnergyServiceData::ServiceTypeSecondary); + QCOMPARE(secondaryData.type(), QLowEnergyServiceData::ServiceTypeSecondary); + + secondaryData.setCharacteristics(QList<QLowEnergyCharacteristicData>() + << charData << QLowEnergyCharacteristicData()); + QCOMPARE(secondaryData.characteristics(), QList<QLowEnergyCharacteristicData>() << charData); + +#ifdef Q_OS_DARWIN + QSKIP("GATT server functionality not implemented for Apple platforms"); +#endif + const QScopedPointer<QLowEnergyController> controller(QLowEnergyController::createPeripheral()); + QVERIFY(!controller->addService(QLowEnergyServiceData())); + const QScopedPointer<QLowEnergyService> secondaryService(controller->addService(secondaryData)); + QVERIFY(!secondaryService.isNull()); + QCOMPARE(secondaryService->serviceUuid(), secondaryData.uuid()); + const QList<QLowEnergyCharacteristic> characteristics = secondaryService->characteristics(); + QCOMPARE(characteristics.count(), 1); + QCOMPARE(characteristics.first().uuid(), charData.uuid()); + const QList<QLowEnergyDescriptor> descriptors = characteristics.first().descriptors(); + QCOMPARE(descriptors.count(), 3); + const auto inUuids = QSet<QBluetoothUuid>() << descData.uuid() << descData2.uuid() + << descData3.uuid(); + QSet<QBluetoothUuid> outUuids; + foreach (const QLowEnergyDescriptor &desc, descriptors) + outUuids << desc.uuid(); + QCOMPARE(inUuids, outUuids); + + QLowEnergyServiceData primaryData; + primaryData.setUuid(QBluetoothUuid::Headset); + primaryData.addIncludedService(secondaryService.data()); + const QScopedPointer<QLowEnergyService> primaryService(controller->addService(primaryData)); + QVERIFY(!primaryService.isNull()); + QCOMPARE(primaryService->characteristics().count(), 0); + const QList<QBluetoothUuid> includedServices = primaryService->includedServices(); + QCOMPARE(includedServices.count(), 1); + QCOMPARE(includedServices.first(), secondaryService->serviceUuid()); +} + +QTEST_MAIN(TestQLowEnergyControllerGattServer) + +#include "tst_qlowenergycontroller-gattserver.moc" diff --git a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp index e85ea44a..a8358dea 100644 --- a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp +++ b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp @@ -252,6 +252,7 @@ void tst_QLowEnergyController::tst_connect() QSKIP("No local Bluetooth or remote BTLE device found. Skipping test."); QLowEnergyController control(remoteDeviceInfo); + QCOMPARE(control.role(), QLowEnergyController::CentralRole); QSignalSpy connectedSpy(&control, SIGNAL(connected())); QSignalSpy disconnectedSpy(&control, SIGNAL(disconnected())); if (remoteDeviceInfo.name().isEmpty()) |