diff options
41 files changed, 5086 insertions, 178 deletions
diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index 67a001d2..920fbe9a 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -24,9 +24,14 @@ PUBLIC_HEADERS += \ qbluetoothtransfermanager.h \ qbluetoothtransferrequest.h \ qlowenergyservice.h \ + qlowenergyservicedata.h \ qlowenergycharacteristic.h \ + qlowenergycharacteristicdata.h \ qlowenergydescriptor.h \ + qlowenergydescriptordata.h \ qbluetoothtransferreply.h \ + qlowenergyadvertisingdata.h \ + qlowenergyadvertisingparameters.h \ qlowenergycontroller.h PRIVATE_HEADERS += \ @@ -43,7 +48,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 +66,14 @@ SOURCES += \ qbluetoothtransfermanager.cpp \ qbluetoothtransferrequest.cpp \ qbluetoothtransferreply.cpp \ + qlowenergyadvertisingdata.cpp \ + qlowenergyadvertisingparameters.cpp \ qlowenergyservice.cpp \ + qlowenergyservicedata.cpp \ qlowenergycharacteristic.cpp \ + qlowenergycharacteristicdata.cpp \ qlowenergydescriptor.cpp \ + qlowenergydescriptordata.cpp \ qlowenergycontroller.cpp \ qlowenergyserviceprivate.cpp @@ -88,6 +99,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 3722b80d..7db0e50b 100644 --- a/src/bluetooth/bluez/bluez_data_p.h +++ b/src/bluetooth/bluez/bluez_data_p.h @@ -134,22 +134,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) @@ -165,15 +149,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) { @@ -191,6 +167,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 @@ -203,12 +193,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; @@ -333,6 +325,36 @@ 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, +}; + +/* 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 30511ae5..388f3e0c 100644 --- a/src/bluetooth/bluez/hcimanager.cpp +++ b/src/bluetooth/bluez/hcimanager.cpp @@ -36,12 +36,14 @@ #include "qbluetoothsocket_p.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) @@ -174,6 +176,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 */ @@ -275,6 +307,18 @@ 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; default: break; } diff --git a/src/bluetooth/bluez/hcimanager_p.h b/src/bluetooth/bluez/hcimanager_p.h index 9dd2ceee..c8f2fe56 100644 --- a/src/bluetooth/bluez/hcimanager_p.h +++ b/src/bluetooth/bluez/hcimanager_p.h @@ -59,6 +59,7 @@ class HciManager : public QObject public: enum HciEvent { EncryptChangeEvent = EVT_ENCRYPT_CHANGE, + CommandCompleteEvent = EVT_CMD_COMPLETE, }; explicit HciManager(const QBluetoothAddress &deviceAdapter, QObject *parent = 0); @@ -66,12 +67,13 @@ 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; - signals: void encryptionChangedEvent(const QBluetoothAddress &address, bool wasSuccess); + void commandCompleted(quint16 opCode, quint8 status, const QByteArray &data); private slots: void _q_readNotify(); diff --git a/src/bluetooth/qbluetooth.cpp b/src/bluetooth/qbluetooth.cpp index 7a1f42ea..03b08bb4 100644 --- a/src/bluetooth/qbluetooth.cpp +++ b/src/bluetooth/qbluetooth.cpp @@ -67,6 +67,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 f448af02..1053b388 100644 --- a/src/bluetooth/qbluetooth.h +++ b/src/bluetooth/qbluetooth.h @@ -39,6 +39,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, @@ -49,6 +53,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 43933f26..6e93f64c 100644 --- a/src/bluetooth/qbluetoothsocket_bluez.cpp +++ b/src/bluetooth/qbluetoothsocket_bluez.cpp @@ -267,9 +267,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 3c8dc786..0808bc98 100644 --- a/src/bluetooth/qbluetoothuuid.cpp +++ b/src/bluetooth/qbluetoothuuid.cpp @@ -1127,11 +1127,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 4646531c..1e8adbb1 100644 --- a/src/bluetooth/qbluetoothuuid.h +++ b/src/bluetooth/qbluetoothuuid.h @@ -373,6 +373,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..4c231dca --- /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 = forceIntoRange(minVal, specMinimum, specMaximum); + params.maxInterval = 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..bdcfcf1c --- /dev/null +++ b/src/bluetooth/qleadvertiser_p.h @@ -0,0 +1,137 @@ +/**************************************************************************** +** +** 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 + +#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/qlowenergycontroller.cpp b/src/bluetooth/qlowenergycontroller.cpp index 3fb68e19..8a6a2e1d 100644 --- a/src/bluetooth/qlowenergycontroller.cpp +++ b/src/bluetooth/qlowenergycontroller.cpp @@ -34,12 +34,19 @@ #include "qlowenergycontroller.h" #include "qlowenergycontroller_p.h" +#include "qlowenergycharacteristicdata.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 @@ -49,9 +56,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 @@ -62,12 +67,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 @@ -93,7 +98,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 */ /*! @@ -113,6 +129,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. */ /*! @@ -128,6 +146,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. */ /*! @@ -135,26 +154,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. */ /*! @@ -181,6 +221,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() */ @@ -191,6 +233,8 @@ 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() */ @@ -224,6 +268,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: @@ -261,6 +308,10 @@ void QLowEnergyControllerPrivate::setState( return; state = newState; + if (state == QLowEnergyController::UnconnectedState + && role == QLowEnergyController::PeripheralRole) { + remoteDevice.clear(); + } emit q->stateChanged(state); } @@ -409,6 +460,7 @@ QLowEnergyController::QLowEnergyController( { Q_D(QLowEnergyController); d->q_ptr = this; + d->role = CentralRole; d->remoteDevice = remoteDevice; d->localAdapter = QBluetoothLocalDevice().address(); d->addressType = QLowEnergyController::PublicAddress; @@ -425,6 +477,7 @@ QLowEnergyController::QLowEnergyController( the connection management. \since 5.5 + \obsolete */ QLowEnergyController::QLowEnergyController( const QBluetoothDeviceInfo &remoteDeviceInfo, @@ -433,6 +486,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; @@ -462,11 +516,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() @@ -492,6 +582,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 { @@ -499,7 +594,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 */ @@ -611,6 +707,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; @@ -619,7 +719,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. @@ -672,6 +773,115 @@ 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); +} + +/*! Returns the last occurred error or \l NoError. */ QLowEnergyController::Error QLowEnergyController::error() const @@ -688,4 +898,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 be729dda..cec0a8cf 100644 --- a/src/bluetooth/qlowenergycontroller.h +++ b/src/bluetooth/qlowenergycontroller.h @@ -38,11 +38,15 @@ #include <QtBluetooth/QBluetoothAddress> #include <QtBluetooth/QBluetoothDeviceInfo> #include <QtBluetooth/QBluetoothUuid> +#include <QtBluetooth/QLowEnergyAdvertisingData> #include <QtBluetooth/QLowEnergyService> QT_BEGIN_NAMESPACE +class QLowEnergyAdvertisingParameters; class QLowEnergyControllerPrivate; +class QLowEnergyServiceData; + class Q_BLUETOOTH_EXPORT QLowEnergyController : public QObject { Q_OBJECT @@ -53,7 +57,8 @@ public: UnknownRemoteDeviceError, NetworkError, InvalidBluetoothAdapterError, - ConnectionError + ConnectionError, + AdvertisingError, }; Q_ENUM(Error) @@ -63,7 +68,8 @@ public: ConnectedState, DiscoveringState, DiscoveredState, - ClosingState + ClosingState, + AdvertisingState, }; Q_ENUM(ControllerState) @@ -73,6 +79,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, @@ -80,6 +89,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; @@ -100,9 +116,18 @@ 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); + Error error() const; QString errorString() const; + Role role() const; + Q_SIGNALS: void connected(); void disconnected(); @@ -113,6 +138,8 @@ Q_SIGNALS: void discoveryFinished(); private: + explicit QLowEnergyController(QObject *parent = 0); // For the peripheral role. + Q_DECLARE_PRIVATE(QLowEnergyController) QLowEnergyControllerPrivate *d_ptr; }; @@ -122,5 +149,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 cb08b6f9..3e3ea830 100644 --- a/src/bluetooth/qlowenergycontroller_android.cpp +++ b/src/bluetooth/qlowenergycontroller_android.cpp @@ -589,4 +589,26 @@ 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::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 a00ac565..a69bd2c4 100644 --- a/src/bluetooth/qlowenergycontroller_bluez.cpp +++ b/src/bluetooth/qlowenergycontroller_bluez.cpp @@ -34,24 +34,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 @@ -59,12 +70,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 @@ -77,6 +92,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 0x2D //GATT command sizes in bytes #define ERROR_RESPONSE_HEADER_SIZE 5 @@ -118,6 +134,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; @@ -195,15 +215,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> >(); @@ -219,6 +268,95 @@ QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() 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::connectToDevice() @@ -296,7 +434,10 @@ void QLowEnergyControllerPrivate::l2cpDisconnected() { Q_Q(QLowEnergyController); - securityLevelValue = -1; + if (role == QLowEnergyController::PeripheralRole) + storeClientConfigurations(); + invalidateServices(); + resetController(); setState(QLowEnergyController::UnconnectedState); emit q->disconnected(); } @@ -333,23 +474,28 @@ void QLowEnergyControllerPrivate::l2cpErrorChanged(QBluetoothSocket::SocketError void QLowEnergyControllerPrivate::resetController() { openRequests.clear(); + openPrepareWriteRequests.clear(); + scheduledIndications.clear(); + indicationInFlight = false; requestPending = false; encryptionChangePending = false; + receivedMtuExchangeRequest = false; securityLevelValue = -1; } 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: @@ -357,31 +503,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; @@ -390,7 +560,7 @@ void QLowEnergyControllerPrivate::l2cpReadyRead() Q_ASSERT(!openRequests.isEmpty()); const Request request = openRequests.dequeue(); - processReply(request, reply); + processReply(request, incomingPacket); sendNextPendingRequest(); } @@ -453,12 +623,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); @@ -475,7 +645,7 @@ void QLowEnergyControllerPrivate::sendNextPendingRequest() // << request.payload.toHex(); requestPending = true; - sendCommand(request.payload); + sendPacket(request.payload); } QLowEnergyHandle parseReadByTypeCharDiscovery( @@ -1072,9 +1242,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); @@ -1110,9 +1280,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); @@ -1206,7 +1376,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); @@ -1261,8 +1431,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); @@ -1325,7 +1495,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); @@ -1443,8 +1613,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); @@ -1479,8 +1649,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; @@ -1557,49 +1727,13 @@ void QLowEnergyControllerPrivate::writeCharacteristic( if (!service->characteristicList.contains(charHandle)) return; - const QLowEnergyHandle valueHandle = service->characteristicList[charHandle].valueHandle; - const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); - - quint8 packet[WRITE_REQUEST_HEADER_SIZE]; - if (writeWithResponse) { - if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { - sendNextPrepareWriteRequest(charHandle, newValue, 0); - sendNextPendingRequest(); - return; - } else { - // write value fits into single package - packet[0] = ATT_OP_WRITE_REQUEST; - } + QLowEnergyServicePrivate::CharData &charData = service->characteristicList[charHandle]; + if (role == QLowEnergyController::PeripheralRole) { + writeCharacteristicForPeripheral(charData, newValue); } else { - // write without response - packet[0] = ATT_OP_WRITE_COMMAND; - } - - bt_put_unaligned(htobs(valueHandle), (quint16 *) &packet[1]); - - QByteArray data(size, Qt::Uninitialized); - memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); - memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); - - qCDebug(QT_BT_BLUEZ) << "Writing characteristic" << hex << charHandle - << "(size:" << size << "with response:" << writeWithResponse << ")"; - - // Advantage of write without response is the quick turnaround. - // It can be send at any time and does not produce responses. - // Therefore we will not put them into the openRequest queue at all. - if (!writeWithResponse) { - sendCommand(data); - return; + writeCharacteristicForCentral(charHandle, charData.valueHandle, newValue, + writeWithResponse); } - - Request request; - request.payload = data; - request.command = ATT_OP_WRITE_REQUEST; - request.reference = charHandle; - request.reference2 = newValue; - openRequests.enqueue(request); - - sendNextPendingRequest(); } void QLowEnergyControllerPrivate::writeDescriptor( @@ -1610,32 +1744,10 @@ void QLowEnergyControllerPrivate::writeDescriptor( { Q_ASSERT(!service.isNull()); - if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { - sendNextPrepareWriteRequest(descriptorHandle, newValue, 0); - sendNextPendingRequest(); - return; - } - - quint8 packet[WRITE_REQUEST_HEADER_SIZE]; - packet[0] = ATT_OP_WRITE_REQUEST; - bt_put_unaligned(htobs(descriptorHandle), (quint16 *) &packet[1]); - - const int size = WRITE_REQUEST_HEADER_SIZE + newValue.size(); - QByteArray data(size, Qt::Uninitialized); - memcpy(data.data(), packet, WRITE_REQUEST_HEADER_SIZE); - memcpy(&(data.data()[WRITE_REQUEST_HEADER_SIZE]), newValue.constData(), newValue.size()); - - qCDebug(QT_BT_BLUEZ) << "Writing descriptor" << hex << descriptorHandle - << "(size:" << size << ")"; - - Request request; - request.payload = data; - request.command = ATT_OP_WRITE_REQUEST; - request.reference = (charHandle | (descriptorHandle << 16)); - request.reference2 = newValue; - openRequests.enqueue(request); - - sendNextPendingRequest(); + if (role == QLowEnergyController::PeripheralRole) + writeDescriptorForPeripheral(service, charHandle, descriptorHandle, newValue); + else + writeDescriptorForCentral(charHandle, descriptorHandle, newValue); } /*! @@ -1662,7 +1774,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); @@ -1697,7 +1809,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); @@ -1746,4 +1858,1092 @@ 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 = packet.at(0) == ATT_OP_SIGNED_WRITE_COMMAND; + if (!checkPacketSize(packet, isSigned ? 15 : 3, mtuSize)) + return; + const QLowEnergyHandle handle = bt_get_le16(packet.constData() + 1); + qCDebug(QT_BT_BLUEZ) << "client sends" << (isSigned ? "signed" : "") << "write" + << (isRequest ? "request" : "command") << "for handle" << handle; + + if (!checkHandle(packet, handle)) + return; + + int valueLength; + if (isSigned) { + // const QByteArray signature = packet.right(12); + return; // TODO: Check signature and continue if it's valid. Store sign counter. + valueLength = packet.count() - 15; + } else { + valueLength = packet.count() - 3; + } + + 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; + } + 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; + + 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 isWriteAccess = type == QLowEnergyCharacteristic::Write + || type == QLowEnergyCharacteristic::WriteNoResponse + || type == QLowEnergyCharacteristic::WriteSigned; + Q_ASSERT(isReadAccess || isWriteAccess); + if (!(attr.properties & type)) + return isReadAccess ? ATT_ERROR_READ_NOT_PERM : 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 a0b33a95..7ca0d9ac 100644 --- a/src/bluetooth/qlowenergycontroller_osx.mm +++ b/src/bluetooth/qlowenergycontroller_osx.mm @@ -933,6 +933,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(); @@ -947,6 +948,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'. @@ -960,6 +962,7 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres { OSX_D_PTR; + osx_d_ptr->role = CentralRole; osx_d_ptr->remoteAddress = remoteAddress; osx_d_ptr->localAddress = localAddress; @@ -967,12 +970,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; @@ -1119,6 +1149,30 @@ 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; +} + 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 4694203a..190f2ac5 100644 --- a/src/bluetooth/qlowenergycontroller_osx_p.h +++ b/src/bluetooth/qlowenergycontroller_osx_p.h @@ -155,6 +155,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 79addae2..c04d0b3c 100644 --- a/src/bluetooth/qlowenergycontroller_p.cpp +++ b/src/bluetooth/qlowenergycontroller_p.cpp @@ -38,7 +38,8 @@ QT_BEGIN_NAMESPACE QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() : QObject(), state(QLowEnergyController::UnconnectedState), - error(QLowEnergyController::NoError) + error(QLowEnergyController::NoError), + lastLocalHandle(0) { registerQLowEnergyControllerMetaType(); } @@ -105,4 +106,19 @@ void QLowEnergyControllerPrivate::writeDescriptor( } +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters &/* params */, + const QLowEnergyAdvertisingData &/* advertisingData */, + const QLowEnergyAdvertisingData &/* scanResponseData */) +{ +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ +} + +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 40318700..f3b28f5c 100644 --- a/src/bluetooth/qlowenergycontroller_p.h +++ b/src/bluetooth/qlowenergycontroller_p.h @@ -65,7 +65,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" @@ -76,10 +78,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 @@ -87,6 +94,7 @@ class LowEnergyNotificationHub; extern void registerQLowEnergyControllerMetaType(); typedef QMap<QBluetoothUuid, QSharedPointer<QLowEnergyServicePrivate> > ServiceDataMap; +class QLeAdvertiser; class QLowEnergyControllerPrivate : public QObject { @@ -109,6 +117,11 @@ public: void discoverServiceDetails(const QBluetoothUuid &service); + void startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData); + void stopAdvertising(); + // misc helpers QSharedPointer<QLowEnergyServicePrivate> serviceForHandle( QLowEnergyHandle handle); @@ -141,9 +154,12 @@ public: const QLowEnergyHandle descriptorHandle, const QByteArray &newValue); + void addToGenericAttributeList(const QLowEnergyServiceData &service, + QLowEnergyHandle startHandle); QBluetoothAddress remoteDevice; QBluetoothAddress localAdapter; + QLowEnergyController::Role role; QString remoteName; @@ -154,6 +170,24 @@ 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: @@ -168,14 +202,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 sendCommand(const QByteArray &packet); + void handleConnectionRequest(); + void closeServerSocket(); + + bool isBonded() const; + QVector<TempClientConfigurationData> gatherClientConfigData(); + void storeClientConfigurations(); + void restoreClientConfigurations(); + + void sendPacket(const QByteArray &packet); void sendNextPendingRequest(); void processReply(const Request &request, const QByteArray &reply); @@ -206,6 +288,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(); @@ -239,6 +388,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 30a66db8..62beff91 100644 --- a/src/bluetooth/qlowenergyservice.cpp +++ b/src/bluetooth/qlowenergyservice.cpp @@ -220,6 +220,9 @@ QT_BEGIN_NAMESPACE \l serviceUuid() and \l serviceName(). \value DiscoveringServices The service details are being discovered. \value ServiceDiscovered The service details have been discovered. + \value LocalService The service is associated with a controller object in the + \l{QLowEnergyController::PeripheralRole}{peripheral role}. Such + service objects do not change their state. */ /*! @@ -302,15 +305,20 @@ QT_BEGIN_NAMESPACE /*! \fn void QLowEnergyService::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) - This signal is emitted when the value of \a characteristic is changed - by an event on the peripheral. The \a newValue parameter contains the - updated value of the \a characteristic. - - The signal emission implies that change notifications must + If the associated controller object is in the \l {QLowEnergyController::CentralRole}{central} + role, this signal is emitted when the value of \a characteristic is changed by an event on the + peripheral. In that case, the signal emission implies that change notifications must have been activated via the characteristic's \l {QBluetoothUuid::ClientCharacteristicConfiguration}{ClientCharacteristicConfiguration} descriptor prior to the change event on the peripheral. More details on how this might be done can be found further \l{notifications}{above}. + + If the controller is in the \l {QLowEnergyController::PeripheralRole}{peripheral} role, that is, + the service object was created via \l QLowEnergyController::addService, the signal is emitted + when a GATT client has written the value of the characteristic using a write request or command. + + The \a newValue parameter contains the updated value of the \a characteristic. + */ /*! @@ -329,8 +337,10 @@ QT_BEGIN_NAMESPACE \fn void QLowEnergyService::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) This signal is emitted when the value of \a descriptor - is successfully changed to \a newValue. The change must have been caused - by calling \l writeDescriptor(). + is successfully changed to \a newValue. If the associated controller object is in the + \l {QLowEnergyController::CentralRole}{central} role, the change must have been caused + by calling \l writeDescriptor(). Otherwise, the signal is the result of a write request or + command from a GATT client to the respective descriptor. \sa writeDescriptor() */ @@ -611,7 +621,13 @@ void QLowEnergyService::readCharacteristic( } /*! - Writes \a newValue as value for the \a characteristic. If the operation is successful, + Writes \a newValue as value for the \a characteristic. The exact semantics depend on + the role that the associated controller object is in. + + \b {Central role} + + The call results in a write request or command to a remote peripheral. + If the operation is successful, the \l characteristicWritten() signal is emitted; otherwise the \l CharacteristicWriteError is set. @@ -640,6 +656,21 @@ void QLowEnergyService::readCharacteristic( characteristic may only support \l WriteWithResponse. If the hardware returns with an error the \l CharacteristicWriteError is set. + \b {Peripheral role} + + The call results in the value of the characteristic getting updated in the local database. + + If a client is currently connected and it has enabled notifications or indications for + the characteristic, the respective information will be sent. + If a device has enabled notifications or indications for the characteristic and that device + is currently not connected, but a bond exists between it and the local device, then + the notification or indication will be sent on the next reconnection. + + If there is a constraint on the length of the characteristic value and \a newValue + does not adhere to that constraint, the behavior is unspecified. + + \note The \a mode argument is ignored in peripheral mode. + \sa QLowEnergyService::characteristicWritten(), QLowEnergyService::readCharacteristic() */ @@ -650,7 +681,10 @@ void QLowEnergyService::writeCharacteristic( //TODO check behavior when writing to WriteSigned characteristic Q_D(QLowEnergyService); - if (d->controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(characteristic)) { + if (d->controller == Q_NULLPTR + || (d->controller->role == QLowEnergyController::CentralRole + && state() != ServiceDiscovered) + || !contains(characteristic)) { d->setError(QLowEnergyService::OperationError); return; } @@ -727,9 +761,14 @@ void QLowEnergyService::readDescriptor( } /*! - Writes \a newValue as value for \a descriptor. If the operation is successful, - the \l descriptorWritten() signal is emitted; otherwise the \l DescriptorWriteError - is emitted. + Writes \a newValue as value for \a descriptor. The exact semantics depend on + the role that the associated controller object is in. + + \b {Central role} + + A call to this function results in a write request to the remote device. + If the operation is successful, the \l descriptorWritten() signal is emitted; otherwise + the \l DescriptorWriteError is emitted. All descriptor and characteristic requests towards the same remote device are serialised. A queue is employed when issuing multiple write requests at the same time. @@ -741,6 +780,11 @@ void QLowEnergyService::readDescriptor( belongs to the service. If one of these conditions is not true the \l QLowEnergyService::OperationError is set. + \b {Peripheral Role} + + The value is written to the local service database. If the contents of \a newValue are not + valid for \a descriptor, the behavior is unspecified. + \sa descriptorWritten(), readDescriptor() */ void QLowEnergyService::writeDescriptor(const QLowEnergyDescriptor &descriptor, @@ -748,7 +792,10 @@ void QLowEnergyService::writeDescriptor(const QLowEnergyDescriptor &descriptor, { Q_D(QLowEnergyService); - if (d->controller == Q_NULLPTR || state() != ServiceDiscovered || !contains(descriptor)) { + if (d->controller == Q_NULLPTR + || (d->controller->role == QLowEnergyController::CentralRole + && state() != ServiceDiscovered) + || !contains(descriptor)) { d->setError(QLowEnergyService::OperationError); return; } diff --git a/src/bluetooth/qlowenergyservice.h b/src/bluetooth/qlowenergyservice.h index 524650c9..6e65aefd 100644 --- a/src/bluetooth/qlowenergyservice.h +++ b/src/bluetooth/qlowenergyservice.h @@ -69,7 +69,8 @@ public: DiscoveryRequired, // we know start/end handle but nothing more //TODO Rename DiscoveringServices -> DiscoveringDetails or DiscoveringService DiscoveringServices,// discoverDetails() called and running - ServiceDiscovered // all details have been synchronized + ServiceDiscovered, // all details have been synchronized + LocalService, }; Q_ENUM(ServiceState) @@ -126,6 +127,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 6f112017..a29d5648 100644 --- a/src/bluetooth/qlowenergyserviceprivate.cpp +++ b/src/bluetooth/qlowenergyserviceprivate.cpp @@ -33,6 +33,8 @@ #include "qlowenergyserviceprivate_p.h" +#include "qlowenergycontroller_p.h" + QT_BEGIN_NAMESPACE QLowEnergyServicePrivate::QLowEnergyServicePrivate(QObject *parent) : diff --git a/src/bluetooth/qlowenergyserviceprivate_p.h b/src/bluetooth/qlowenergyserviceprivate_p.h index fc2276e6..c17932cf 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -51,10 +51,10 @@ #include <QtBluetooth/QLowEnergyService> #include <QtBluetooth/QLowEnergyCharacteristic> -#include "qlowenergycontroller_p.h" - QT_BEGIN_NAMESPACE +class QLowEnergyControllerPrivate; + class QLowEnergyServicePrivate : public QObject { Q_OBJECT diff --git a/tests/auto/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..e2c930bf --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp @@ -0,0 +1,220 @@ +/**************************************************************************** +** +** 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/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"); + }; + 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..6c45eed7 --- /dev/null +++ b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp @@ -0,0 +1,492 @@ +/**************************************************************************** +** +** 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/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 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::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"); + + 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 b31b35f0..1386f75c 100644 --- a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp +++ b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp @@ -257,6 +257,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()) |