| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017 The Qt Company Ltd. |
| ** Copyright (C) 2016 Javier S. Pedro <maemo@javispedro.com> |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtBluetooth module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** 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 https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://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 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "lecmaccalculator_p.h" |
| #include "qlowenergycontroller_bluez_p.h" |
| #include "qbluetoothsocketbase_p.h" |
| #include "qbluetoothsocket_bluez_p.h" |
| #include "qleadvertiser_p.h" |
| #include "bluez/bluez_data_p.h" |
| #include "bluez/hcimanager_p.h" |
| #include "bluez/objectmanager_p.h" |
| #include "bluez/remotedevicemanager_p.h" |
| #include "bluez/bluez5_helper_p.h" |
| #include "bluez/bluetoothmanagement_p.h" |
| |
| // Bluez 4 |
| #include "bluez/adapter_p.h" |
| #include "bluez/device_p.h" |
| #include "bluez/manager_p.h" |
| |
| #include <QtCore/QFileInfo> |
| #include <QtCore/QLoggingCategory> |
| #include <QtCore/QSettings> |
| #include <QtCore/QTimer> |
| #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 ATT_DEFAULT_LE_MTU 23 |
| #define ATT_MAX_LE_MTU 0x200 |
| |
| #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 |
| #define ATT_OP_EXCHANGE_MTU_REQUEST 0x2 //send own mtu |
| #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 |
| #define ATT_OP_WRITE_RESPONSE 0x13 |
| #define ATT_OP_PREPARE_WRITE_REQUEST 0x16 //write values longer than MTU-3 -> queueing |
| #define ATT_OP_PREPARE_WRITE_RESPONSE 0x17 |
| #define ATT_OP_EXECUTE_WRITE_REQUEST 0x18 //write values longer than MTU-3 -> execute queue |
| #define ATT_OP_EXECUTE_WRITE_RESPONSE 0x19 |
| #define ATT_OP_HANDLE_VAL_NOTIFICATION 0x1b //informs about value change |
| #define ATT_OP_HANDLE_VAL_INDICATION 0x1d //informs about value change -> requires reply |
| #define ATT_OP_HANDLE_VAL_CONFIRMATION 0x1e //answer for ATT_OP_HANDLE_VAL_INDICATION |
| #define ATT_OP_WRITE_COMMAND 0x52 //write characteristic without response |
| #define ATT_OP_SIGNED_WRITE_COMMAND 0xD2 |
| |
| //GATT command sizes in bytes |
| #define ERROR_RESPONSE_HEADER_SIZE 5 |
| #define FIND_INFO_REQUEST_HEADER_SIZE 5 |
| #define GRP_TYPE_REQ_HEADER_SIZE 7 |
| #define READ_BY_TYPE_REQ_HEADER_SIZE 7 |
| #define READ_REQUEST_HEADER_SIZE 3 |
| #define READ_BLOB_REQUEST_HEADER_SIZE 5 |
| #define WRITE_REQUEST_HEADER_SIZE 3 // same size for WRITE_COMMAND header |
| #define PREPARE_WRITE_HEADER_SIZE 5 |
| #define EXECUTE_WRITE_HEADER_SIZE 2 |
| #define MTU_EXCHANGE_HEADER_SIZE 3 |
| |
| // GATT error codes |
| #define ATT_ERROR_INVALID_HANDLE 0x01 |
| #define ATT_ERROR_READ_NOT_PERM 0x02 |
| #define ATT_ERROR_WRITE_NOT_PERM 0x03 |
| #define ATT_ERROR_INVALID_PDU 0x04 |
| #define ATT_ERROR_INSUF_AUTHENTICATION 0x05 |
| #define ATT_ERROR_REQUEST_NOT_SUPPORTED 0x06 |
| #define ATT_ERROR_INVALID_OFFSET 0x07 |
| #define ATT_ERROR_INSUF_AUTHORIZATION 0x08 |
| #define ATT_ERROR_PREPARE_QUEUE_FULL 0x09 |
| #define ATT_ERROR_ATTRIBUTE_NOT_FOUND 0x0A |
| #define ATT_ERROR_ATTRIBUTE_NOT_LONG 0x0B |
| #define ATT_ERROR_INSUF_ENCR_KEY_SIZE 0x0C |
| #define ATT_ERROR_INVAL_ATTR_VALUE_LEN 0x0D |
| #define ATT_ERROR_UNLIKELY 0x0E |
| #define ATT_ERROR_INSUF_ENCRYPTION 0x0F |
| #define ATT_ERROR_UNSUPPRTED_GROUP_TYPE 0x10 |
| #define ATT_ERROR_INSUF_RESOURCES 0x11 |
| #define ATT_ERROR_APPLICATION_START 0x80 |
| //------------------------------------------ |
| // The error codes in this block are |
| // implementation specific errors |
| |
| #define ATT_ERROR_REQUEST_STALLED 0x81 |
| |
| //------------------------------------------ |
| #define ATT_ERROR_APPLICATION_END 0x9f |
| |
| #define APPEND_VALUE true |
| #define NEW_VALUE false |
| |
| 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; |
| |
| // Bluetooth LE data comes as little endian |
| // uuids are constructed using high endian |
| btoh128(p, &dst_hostOrder); |
| hton128(&dst_hostOrder, &dst_bigEndian); |
| |
| // convert to Qt's own data type |
| quint128 qtdst; |
| memcpy(&qtdst, &dst_bigEndian, sizeof(quint128)); |
| |
| return QBluetoothUuid(qtdst); |
| } |
| |
| static void dumpErrorInformation(const QByteArray &response) |
| { |
| const char *data = response.constData(); |
| if (response.size() != 5 || data[0] != ATT_OP_ERROR_RESPONSE) { |
| qCWarning(QT_BT_BLUEZ) << QLatin1String("Not a valid error response"); |
| return; |
| } |
| |
| quint8 lastCommand = data[1]; |
| quint16 handle = bt_get_le16(&data[2]); |
| quint8 errorCode = data[4]; |
| |
| QString errorString; |
| switch (errorCode) { |
| case ATT_ERROR_INVALID_HANDLE: |
| errorString = QStringLiteral("invalid handle"); break; |
| case ATT_ERROR_READ_NOT_PERM: |
| errorString = QStringLiteral("not readable attribute - permissions"); break; |
| case ATT_ERROR_WRITE_NOT_PERM: |
| errorString = QStringLiteral("not writable attribute - permissions"); break; |
| case ATT_ERROR_INVALID_PDU: |
| errorString = QStringLiteral("PDU invalid"); break; |
| case ATT_ERROR_INSUF_AUTHENTICATION: |
| errorString = QStringLiteral("needs authentication - permissions"); break; |
| case ATT_ERROR_REQUEST_NOT_SUPPORTED: |
| errorString = QStringLiteral("server does not support request"); break; |
| case ATT_ERROR_INVALID_OFFSET: |
| errorString = QStringLiteral("offset past end of attribute"); break; |
| case ATT_ERROR_INSUF_AUTHORIZATION: |
| errorString = QStringLiteral("need authorization - permissions"); break; |
| case ATT_ERROR_PREPARE_QUEUE_FULL: |
| errorString = QStringLiteral("run out of prepare queue space"); break; |
| case ATT_ERROR_ATTRIBUTE_NOT_FOUND: |
| errorString = QStringLiteral("no attribute in given range found"); break; |
| case ATT_ERROR_ATTRIBUTE_NOT_LONG: |
| errorString = QStringLiteral("attribute not read/written using read blob"); break; |
| case ATT_ERROR_INSUF_ENCR_KEY_SIZE: |
| errorString = QStringLiteral("need encryption key size - permissions"); break; |
| case ATT_ERROR_INVAL_ATTR_VALUE_LEN: |
| errorString = QStringLiteral("written value is invalid size"); break; |
| case ATT_ERROR_UNLIKELY: |
| errorString = QStringLiteral("unlikely error"); break; |
| case ATT_ERROR_INSUF_ENCRYPTION: |
| errorString = QStringLiteral("needs encryption - permissions"); break; |
| case ATT_ERROR_UNSUPPRTED_GROUP_TYPE: |
| errorString = QStringLiteral("unsupported group type"); break; |
| case ATT_ERROR_INSUF_RESOURCES: |
| errorString = QStringLiteral("insufficient resources to complete request"); break; |
| default: |
| if (errorCode >= ATT_ERROR_APPLICATION_START && errorCode <= ATT_ERROR_APPLICATION_END) |
| errorString = QStringLiteral("application error: %1").arg(errorCode); |
| else |
| errorString = QStringLiteral("unknown error code"); |
| break; |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << "Error1:" << errorString |
| << "last command:" << hex << lastCommand |
| << "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 { |
| quint128 hostOrder; |
| quint128 qtUuidOrder = uuid.toUInt128(); |
| ntoh128(&qtUuidOrder, &hostOrder); |
| putBtData(hostOrder, dst); |
| } |
| dst += uuidSize; |
| } |
| template<> void putDataAndIncrement(const QByteArray &value, char *&dst) |
| { |
| using namespace std; |
| memcpy(dst, value.constData(), value.count()); |
| dst += value.count(); |
| } |
| |
| QLowEnergyControllerPrivateBluez::QLowEnergyControllerPrivateBluez() |
| : QLowEnergyControllerPrivate(), |
| requestPending(false), |
| mtuSize(ATT_DEFAULT_LE_MTU), |
| securityLevelValue(-1), |
| encryptionChangePending(false) |
| { |
| registerQLowEnergyControllerMetaType(); |
| qRegisterMetaType<QList<QLowEnergyHandle> >(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::init() |
| { |
| hciManager = new HciManager(localAdapter, this); |
| if (!hciManager->isValid()) |
| return; |
| |
| hciManager->monitorEvent(HciManager::EncryptChangeEvent); |
| connect(hciManager, SIGNAL(encryptionChangedEvent(QBluetoothAddress,bool)), |
| this, SLOT(encryptionChangedEvent(QBluetoothAddress,bool))); |
| hciManager->monitorEvent(HciManager::LeMetaEvent); |
| hciManager->monitorAclPackets(); |
| connect(hciManager, &HciManager::connectionComplete, [this](quint16 handle) { |
| connectionHandle = handle; |
| qCDebug(QT_BT_BLUEZ) << "received connection complete event, handle:" << handle; |
| }); |
| connect(hciManager, &HciManager::connectionUpdate, |
| [this](quint16 handle, const QLowEnergyConnectionParameters ¶ms) { |
| if (handle == connectionHandle) |
| emit q_ptr->connectionUpdated(params); |
| } |
| ); |
| connect(hciManager, &HciManager::signatureResolvingKeyReceived, |
| [this](quint16 handle, bool remoteKey, const quint128 &csrk) { |
| if (handle != connectionHandle) |
| return; |
| if ((remoteKey && role == QLowEnergyController::CentralRole) |
| || (!remoteKey && role == QLowEnergyController::PeripheralRole)) { |
| return; |
| } |
| qCDebug(QT_BT_BLUEZ) << "received new signature resolving key" |
| << QByteArray(reinterpret_cast<const char *>(csrk.data), |
| sizeof csrk).toHex(); |
| signingData.insert(remoteDevice.toUInt64(), SigningData(csrk)); |
| } |
| ); |
| |
| if (role == QLowEnergyController::CentralRole) { |
| if (Q_UNLIKELY(!qEnvironmentVariableIsEmpty("BLUETOOTH_GATT_TIMEOUT"))) { |
| bool ok = false; |
| int value = qEnvironmentVariableIntValue("BLUETOOTH_GATT_TIMEOUT", &ok); |
| if (ok) |
| gattRequestTimeout = value; |
| } |
| |
| // permit disabling of timeout behavior via environment variable |
| if (gattRequestTimeout > 0) { |
| qCWarning(QT_BT_BLUEZ) << "Enabling GATT request timeout behavior" << gattRequestTimeout; |
| requestTimer = new QTimer(this); |
| requestTimer->setSingleShot(true); |
| requestTimer->setInterval(gattRequestTimeout); |
| connect(requestTimer, &QTimer::timeout, |
| this, &QLowEnergyControllerPrivateBluez::handleGattRequestTimeout); |
| qRegisterMetaTypeStreamOperators<QBluetoothUuid>(); |
| } |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::handleGattRequestTimeout() |
| { |
| // antyhing open that might require cancellation or a warning? |
| if (encryptionChangePending) { |
| // We cannot really recover for now but the warning is essential for debugging |
| qCWarning(QT_BT_BLUEZ) << "****** Encryption change event blocking further GATT requests"; |
| return; |
| } |
| |
| if (!openRequests.isEmpty() && requestPending) { |
| const Request currentRequest = openRequests.dequeue(); |
| requestPending = false; // reset pending flag |
| |
| qCWarning(QT_BT_BLUEZ).nospace() << "****** Request type 0x" << hex << currentRequest.command |
| << " to server/peripheral timed out"; |
| qCWarning(QT_BT_BLUEZ) << "****** Looks like the characteristic or descriptor does NOT act in" |
| << "accordance to Bluetooth 4.x spec."; |
| qCWarning(QT_BT_BLUEZ) << "****** Please check server implementation." |
| << "Continuing under reservation."; |
| |
| quint8 command = currentRequest.command; |
| const auto createRequestErrorMessage = [](quint8 opcodeWithError, |
| QLowEnergyHandle handle) { |
| QByteArray errorPackage(ERROR_RESPONSE_HEADER_SIZE, Qt::Uninitialized); |
| errorPackage[0] = ATT_OP_ERROR_RESPONSE; |
| errorPackage[1] = opcodeWithError; // e.g. ATT_OP_READ_REQUEST |
| putBtData(handle, errorPackage.data() + 2); // |
| errorPackage[4] = ATT_ERROR_REQUEST_STALLED; |
| |
| return errorPackage; |
| }; |
| |
| switch (command) { |
| case ATT_OP_EXCHANGE_MTU_REQUEST: // MTU change request |
| // never received reply to MTU request |
| // it is safe to skip and go to next request |
| break; |
| case ATT_OP_READ_BY_GROUP_REQUEST: // primary or secondary service discovery |
| case ATT_OP_READ_BY_TYPE_REQUEST: // characteristic or included service discovery |
| // jump back into usual response handling with custom error code |
| // 2nd param "0" as required by spec |
| processReply(currentRequest, createRequestErrorMessage(command, 0)); |
| break; |
| case ATT_OP_READ_REQUEST: // read descriptor or characteristic value |
| case ATT_OP_READ_BLOB_REQUEST: // read long descriptor or characteristic |
| case ATT_OP_WRITE_REQUEST: // write descriptor or characteristic |
| { |
| uint handleData = currentRequest.reference.toUInt(); |
| const QLowEnergyHandle charHandle = (handleData & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((handleData >> 16) & 0xffff); |
| processReply(currentRequest, createRequestErrorMessage(command, |
| descriptorHandle ? descriptorHandle : charHandle)); |
| } |
| break; |
| case ATT_OP_FIND_INFORMATION_REQUEST: // get descriptor information |
| processReply(currentRequest, createRequestErrorMessage( |
| command, currentRequest.reference2.toUInt())); |
| break; |
| case ATT_OP_PREPARE_WRITE_REQUEST: // prepare to write long desc or char |
| case ATT_OP_EXECUTE_WRITE_REQUEST: // execute long write of desc or char |
| { |
| uint handleData = currentRequest.reference.toUInt(); |
| const QLowEnergyHandle attrHandle = (handleData & 0xffff); |
| processReply(currentRequest, |
| createRequestErrorMessage(command, attrHandle)); |
| } |
| break; |
| default: |
| // not a command used by central role implementation |
| qCWarning(QT_BT_BLUEZ) << "Missing response for ATT peripheral command: " |
| << hex << command; |
| break; |
| } |
| |
| // spin openRequest queue further |
| sendNextPendingRequest(); |
| } |
| } |
| |
| QLowEnergyControllerPrivateBluez::~QLowEnergyControllerPrivateBluez() |
| { |
| closeServerSocket(); |
| delete cmacCalculator; |
| } |
| |
| 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 QLowEnergyControllerPrivateBluez::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, |
| &QLowEnergyControllerPrivateBluez::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, |
| &QLowEnergyControllerPrivateBluez::handleConnectionRequest); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::stopAdvertising() |
| { |
| setState(QLowEnergyController::UnconnectedState); |
| advertiser->stopAdvertising(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) |
| { |
| // The spec says that the connection update command can be used by both slave and master |
| // devices, but BlueZ allows it only for master devices. So for slave devices, we have to use a |
| // connection parameter update request, which we need to wrap in an ACL command, as BlueZ |
| // does not allow user-space sockets for the signaling channel. |
| if (role == QLowEnergyController::CentralRole) |
| hciManager->sendConnectionUpdateCommand(connectionHandle, params); |
| else |
| hciManager->sendConnectionParameterUpdateRequest(connectionHandle, params); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::connectToDevice() |
| { |
| if (remoteDevice.isNull()) { |
| qCWarning(QT_BT_BLUEZ) << "Invalid/null remote device address"; |
| setError(QLowEnergyController::UnknownRemoteDeviceError); |
| return; |
| } |
| |
| setState(QLowEnergyController::ConnectingState); |
| if (l2cpSocket) { |
| delete l2cpSocket; |
| l2cpSocket = nullptr; |
| } |
| |
| createServicesForCentralIfRequired(); |
| |
| // check for active running connections |
| // BlueZ 5.37+ (maybe even earlier versions) can have pending BTLE connections |
| // Only one active L2CP socket to CID 0x4 possible at a time |
| // this check is not performed for BlueZ 4 based platforms as bluetoothd |
| // does not support BTLE management |
| |
| if (!isBluez5()) { |
| establishL2cpClientSocket(); |
| return; |
| } |
| |
| QVector<quint16> activeHandles = hciManager->activeLowEnergyConnections(); |
| if (!activeHandles.isEmpty()) { |
| qCWarning(QT_BT_BLUEZ) << "Cannot connect due to pending active LE connections"; |
| |
| if (!device1Manager) { |
| device1Manager = new RemoteDeviceManager(localAdapter, this); |
| connect(device1Manager, &RemoteDeviceManager::finished, |
| this, &QLowEnergyControllerPrivateBluez::activeConnectionTerminationDone); |
| } |
| |
| QVector<QBluetoothAddress> connectedAddresses; |
| for (const auto handle: activeHandles) { |
| const QBluetoothAddress addr = hciManager->addressForConnectionHandle(handle); |
| if (!addr.isNull()) |
| connectedAddresses.push_back(addr); |
| } |
| device1Manager->scheduleJob(RemoteDeviceManager::JobType::JobDisconnectDevice, connectedAddresses); |
| } else { |
| establishL2cpClientSocket(); |
| } |
| } |
| |
| /*! |
| * Handles outcome of attempts to close external connections. |
| */ |
| void QLowEnergyControllerPrivateBluez::activeConnectionTerminationDone() |
| { |
| if (!device1Manager) |
| return; |
| |
| qCDebug(QT_BT_BLUEZ) << "RemoteDeviceManager finished attempting" |
| << "to close external connections"; |
| |
| QVector<quint16> activeHandles = hciManager->activeLowEnergyConnections(); |
| if (!activeHandles.isEmpty()) { |
| qCWarning(QT_BT_BLUEZ) << "Cannot close pending external BTLE connections. Aborting connect attempt"; |
| setError(QLowEnergyController::ConnectionError); |
| setState(QLowEnergyController::UnconnectedState); |
| l2cpDisconnected(); |
| return; |
| } else { |
| establishL2cpClientSocket(); |
| } |
| } |
| |
| /*! |
| * Establishes the L2CP client socket. |
| */ |
| void QLowEnergyControllerPrivateBluez::establishL2cpClientSocket() |
| { |
| //we are already in Connecting state |
| |
| l2cpSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol, this); |
| connect(l2cpSocket, SIGNAL(connected()), this, SLOT(l2cpConnected())); |
| connect(l2cpSocket, SIGNAL(disconnected()), this, SLOT(l2cpDisconnected())); |
| connect(l2cpSocket, SIGNAL(error(QBluetoothSocket::SocketError)), |
| this, SLOT(l2cpErrorChanged(QBluetoothSocket::SocketError))); |
| connect(l2cpSocket, SIGNAL(readyRead()), this, SLOT(l2cpReadyRead())); |
| |
| quint32 addressTypeToUse = (addressType == QLowEnergyController::PublicAddress) |
| ? BDADDR_LE_PUBLIC : BDADDR_LE_RANDOM; |
| if (BluetoothManagement::instance()->isMonitoringEnabled()) { |
| // if monitoring is possible and it's private then we force it to the relevant option |
| if (BluetoothManagement::instance()->isAddressRandom(remoteDevice)) { |
| addressTypeToUse = BDADDR_LE_RANDOM; |
| } |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << "addresstypeToUse:" |
| << (addressTypeToUse == BDADDR_LE_RANDOM |
| ? QStringLiteral("Random") : QStringLiteral("Public")); |
| |
| l2cpSocket->d_ptr->lowEnergySocketType = addressTypeToUse; |
| |
| int sockfd = l2cpSocket->socketDescriptor(); |
| if (sockfd < 0) { |
| qCWarning(QT_BT_BLUEZ) << "l2cp socket not initialised"; |
| setError(QLowEnergyController::ConnectionError); |
| setState(QLowEnergyController::UnconnectedState); |
| return; |
| } |
| |
| struct sockaddr_l2 addr; |
| 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); |
| |
| // bind the socket to the local device |
| if (::bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { |
| qCWarning(QT_BT_BLUEZ) << qt_error_string(errno); |
| setError(QLowEnergyController::ConnectionError); |
| setState(QLowEnergyController::UnconnectedState); |
| return; |
| } |
| |
| // connect |
| // Unbuffered mode required to separate each GATT packet |
| l2cpSocket->connectToService(remoteDevice, ATTRIBUTE_CHANNEL_ID, |
| QIODevice::ReadWrite | QIODevice::Unbuffered); |
| loadSigningDataIfNecessary(LocalSigningKey); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::createServicesForCentralIfRequired() |
| { |
| bool ok = false; |
| int value = qEnvironmentVariableIntValue("QT_DEFAULT_CENTRAL_SERVICES", &ok); |
| if (Q_UNLIKELY(ok && value == 0)) |
| return; //nothing to do |
| |
| //do not add the services each time we start a connection |
| if (localServices.contains(QBluetoothUuid(QBluetoothUuid::GenericAccess))) |
| return; |
| |
| qCDebug(QT_BT_BLUEZ) << "Creating default GAP/GATT services"; |
| |
| //populate Generic Access service |
| //for now the values are static |
| QLowEnergyServiceData gapServiceData; |
| gapServiceData.setType(QLowEnergyServiceData::ServiceTypePrimary); |
| gapServiceData.setUuid(QBluetoothUuid::GenericAccess); |
| |
| QLowEnergyCharacteristicData gapDeviceName; |
| gapDeviceName.setUuid(QBluetoothUuid::DeviceName); |
| gapDeviceName.setProperties(QLowEnergyCharacteristic::Read); |
| |
| QBluetoothLocalDevice mainAdapter; |
| gapDeviceName.setValue(mainAdapter.name().toLatin1()); //static name |
| |
| QLowEnergyCharacteristicData gapAppearance; |
| gapAppearance.setUuid(QBluetoothUuid::Appearance); |
| gapAppearance.setProperties(QLowEnergyCharacteristic::Read); |
| gapAppearance.setValue(QByteArray::fromHex("80")); // Generic Computer (0x80) |
| |
| QLowEnergyCharacteristicData gapPrivacyFlag; |
| gapPrivacyFlag.setUuid(QBluetoothUuid::PeripheralPrivacyFlag); |
| gapPrivacyFlag.setProperties(QLowEnergyCharacteristic::Read); |
| gapPrivacyFlag.setValue(QByteArray::fromHex("00")); // disable privacy |
| |
| gapServiceData.addCharacteristic(gapDeviceName); |
| gapServiceData.addCharacteristic(gapAppearance); |
| gapServiceData.addCharacteristic(gapPrivacyFlag); |
| |
| Q_Q(QLowEnergyController); |
| QLowEnergyService *service = addServiceHelper(gapServiceData); |
| if (service) |
| service->setParent(q); |
| |
| QLowEnergyServiceData gattServiceData; |
| gattServiceData.setType(QLowEnergyServiceData::ServiceTypePrimary); |
| gattServiceData.setUuid(QBluetoothUuid::GenericAttribute); |
| |
| QLowEnergyCharacteristicData serviceChangedChar; |
| serviceChangedChar.setUuid(QBluetoothUuid::ServiceChanged); |
| serviceChangedChar.setProperties(QLowEnergyCharacteristic::Indicate); |
| //arbitrary range of 2 bit handle range (1-4 |
| serviceChangedChar.setValue(QByteArray::fromHex("0104")); |
| |
| const QLowEnergyDescriptorData clientConfig( |
| QBluetoothUuid::ClientCharacteristicConfiguration, |
| QByteArray(2, 0)); |
| serviceChangedChar.addDescriptor(clientConfig); |
| gattServiceData.addCharacteristic(serviceChangedChar); |
| |
| service = addServiceHelper(gattServiceData); |
| if (service) |
| service->setParent(q); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::l2cpConnected() |
| { |
| Q_Q(QLowEnergyController); |
| |
| securityLevelValue = securityLevel(); |
| exchangeMTU(); |
| |
| setState(QLowEnergyController::ConnectedState); |
| emit q->connected(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::disconnectFromDevice() |
| { |
| setState(QLowEnergyController::ClosingState); |
| if (l2cpSocket) |
| l2cpSocket->close(); |
| resetController(); |
| |
| // this may happen when RemoteDeviceManager::JobType::JobDisconnectDevice |
| // is pending. |
| if (!l2cpSocket) { |
| qWarning(QT_BT_BLUEZ) << "Unexpected closure of device. Cleaning up internal states."; |
| l2cpDisconnected(); |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::l2cpDisconnected() |
| { |
| Q_Q(QLowEnergyController); |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| storeClientConfigurations(); |
| remoteDevice.clear(); |
| remoteName.clear(); |
| } |
| invalidateServices(); |
| resetController(); |
| setState(QLowEnergyController::UnconnectedState); |
| emit q->disconnected(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::l2cpErrorChanged(QBluetoothSocket::SocketError e) |
| { |
| switch (e) { |
| case QBluetoothSocket::HostNotFoundError: |
| setError(QLowEnergyController::UnknownRemoteDeviceError); |
| qCDebug(QT_BT_BLUEZ) << "The passed remote device address cannot be found"; |
| break; |
| case QBluetoothSocket::NetworkError: |
| setError(QLowEnergyController::NetworkError); |
| qCDebug(QT_BT_BLUEZ) << "Network IO error while talking to LE device"; |
| break; |
| case QBluetoothSocket::RemoteHostClosedError: |
| setError(QLowEnergyController::RemoteHostClosedError); |
| qCDebug(QT_BT_BLUEZ) << "Remote host closed the connection"; |
| break; |
| case QBluetoothSocket::UnknownSocketError: |
| case QBluetoothSocket::UnsupportedProtocolError: |
| case QBluetoothSocket::OperationError: |
| case QBluetoothSocket::ServiceNotFoundError: |
| default: |
| // these errors shouldn't happen -> as it means |
| // the code in this file has bugs |
| qCDebug(QT_BT_BLUEZ) << "Unknown l2cp socket error: " << e << l2cpSocket->errorString(); |
| setError(QLowEnergyController::UnknownError); |
| break; |
| } |
| |
| invalidateServices(); |
| resetController(); |
| setState(QLowEnergyController::UnconnectedState); |
| } |
| |
| |
| void QLowEnergyControllerPrivateBluez::resetController() |
| { |
| openRequests.clear(); |
| openPrepareWriteRequests.clear(); |
| scheduledIndications.clear(); |
| indicationInFlight = false; |
| requestPending = false; |
| encryptionChangePending = false; |
| receivedMtuExchangeRequest = false; |
| mtuSize = ATT_DEFAULT_LE_MTU; |
| securityLevelValue = -1; |
| connectionHandle = 0; |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| // public API behavior requires stop of advertisement |
| if (advertiser) { |
| advertiser->stopAdvertising(); |
| delete advertiser; |
| advertiser = nullptr; |
| } |
| localAttributes.clear(); |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::restartRequestTimer() |
| { |
| if (!requestTimer) |
| return; |
| |
| if (gattRequestTimeout > 0) |
| requestTimer->start(gattRequestTimeout); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::l2cpReadyRead() |
| { |
| const QByteArray incomingPacket = l2cpSocket->readAll(); |
| qCDebug(QT_BT_BLUEZ) << "Received size:" << incomingPacket.size() << "data:" |
| << incomingPacket.toHex(); |
| if (incomingPacket.isEmpty()) |
| return; |
| |
| const quint8 command = incomingPacket.constData()[0]; |
| switch (command) { |
| case ATT_OP_HANDLE_VAL_NOTIFICATION: |
| { |
| processUnsolicitedReply(incomingPacket); |
| return; |
| } |
| case ATT_OP_HANDLE_VAL_INDICATION: |
| { |
| //send confirmation |
| QByteArray packet; |
| packet.append(static_cast<char>(ATT_OP_HANDLE_VAL_CONFIRMATION)); |
| sendPacket(packet); |
| |
| processUnsolicitedReply(incomingPacket); |
| return; |
| } |
| //-------------------------------------------------- |
| // Peripheral side packet handling |
| case ATT_OP_EXCHANGE_MTU_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: |
| 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: |
| 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; |
| break; |
| } |
| |
| if (openRequests.isEmpty()) { |
| qCWarning(QT_BT_BLUEZ) << "Received unexpected packet from peer, disconnecting."; |
| disconnectFromDevice(); |
| return; |
| } |
| |
| const Request request = openRequests.dequeue(); |
| processReply(request, incomingPacket); |
| |
| sendNextPendingRequest(); |
| } |
| |
| /*! |
| * Called when the request for socket encryption has been |
| * processed by the kernel. Such requests take time as the kernel |
| * has to renegotiate the link parameters with the remote device. |
| * |
| * Therefore any such request delays the pending ATT commands until this |
| * callback is called. The first pending request in the queue is the request |
| * that triggered the encryption request. |
| */ |
| void QLowEnergyControllerPrivateBluez::encryptionChangedEvent( |
| const QBluetoothAddress &address, bool wasSuccess) |
| { |
| if (!encryptionChangePending) // somebody else caused change event |
| return; |
| |
| if (remoteDevice != address) |
| return; |
| |
| securityLevelValue = securityLevel(); |
| |
| // On success continue to process ATT command queue |
| if (!wasSuccess) { |
| // We could not increase the security of the link |
| // The next request was requeued due to security error |
| // skip it to avoid endless loop of security negotiations |
| Q_ASSERT(!openRequests.isEmpty()); |
| Request failedRequest = openRequests.takeFirst(); |
| |
| if (failedRequest.command == ATT_OP_WRITE_REQUEST) { |
| // Failing write requests trigger some sort of response |
| uint ref = failedRequest.reference.toUInt(); |
| const QLowEnergyHandle charHandle = (ref & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((ref >> 16) & 0xffff); |
| |
| QSharedPointer<QLowEnergyServicePrivate> service |
| = serviceForHandle(charHandle); |
| if (!service.isNull() && service->characteristicList.contains(charHandle)) { |
| if (!descriptorHandle) |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| else |
| service->setError(QLowEnergyService::DescriptorWriteError); |
| } |
| } else if (failedRequest.command == ATT_OP_PREPARE_WRITE_REQUEST) { |
| uint handleData = failedRequest.reference.toUInt(); |
| const QLowEnergyHandle attrHandle = (handleData & 0xffff); |
| const QByteArray newValue = failedRequest.reference2.toByteArray(); |
| |
| // Prepare command failed, cancel pending prepare queue on |
| // the device. The appropriate (Descriptor|Characteristic)WriteError |
| // is emitted too once the execute write request comes through |
| sendExecuteWriteRequest(attrHandle, newValue, true); |
| } |
| } |
| |
| encryptionChangePending = false; |
| sendNextPendingRequest(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendPacket(const QByteArray &packet) |
| { |
| qint64 result = l2cpSocket->write(packet.constData(), |
| packet.size()); |
| // We ignore result == 0 which is likely to be caused by EAGAIN. |
| // This packet is effectively discarded but the controller can still recover |
| |
| if (result == -1) { |
| qCDebug(QT_BT_BLUEZ) << "Cannot write L2CP packet:" << hex |
| << packet.toHex() |
| << l2cpSocket->errorString(); |
| setError(QLowEnergyController::NetworkError); |
| } else if (result < packet.size()) { |
| qCWarning(QT_BT_BLUEZ) << "L2CP write request incomplete:" |
| << result << "of" << packet.size(); |
| } |
| |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendNextPendingRequest() |
| { |
| if (openRequests.isEmpty() || requestPending || encryptionChangePending) |
| return; |
| |
| const Request &request = openRequests.head(); |
| // qCDebug(QT_BT_BLUEZ) << "Sending request, type:" << hex << request.command |
| // << request.payload.toHex(); |
| |
| requestPending = true; |
| restartRequestTimer(); |
| sendPacket(request.payload); |
| } |
| |
| QLowEnergyHandle parseReadByTypeCharDiscovery( |
| QLowEnergyServicePrivate::CharData *charData, |
| const char *data, quint16 elementLength) |
| { |
| Q_ASSERT(charData); |
| Q_ASSERT(data); |
| |
| QLowEnergyHandle attributeHandle = bt_get_le16(&data[0]); |
| charData->properties = |
| (QLowEnergyCharacteristic::PropertyTypes)(data[2] & 0xff); |
| charData->valueHandle = bt_get_le16(&data[3]); |
| |
| if (elementLength == 7) // 16 bit uuid |
| charData->uuid = QBluetoothUuid(bt_get_le16(&data[5])); |
| else |
| charData->uuid = convert_uuid128((quint128 *)&data[5]); |
| |
| qCDebug(QT_BT_BLUEZ) << "Found handle:" << hex << attributeHandle |
| << "properties:" << charData->properties |
| << "value handle:" << charData->valueHandle |
| << "uuid:" << charData->uuid.toString(); |
| |
| return attributeHandle; |
| } |
| |
| QLowEnergyHandle parseReadByTypeIncludeDiscovery( |
| QList<QBluetoothUuid> *foundServices, |
| const char *data, quint16 elementLength) |
| { |
| Q_ASSERT(foundServices); |
| Q_ASSERT(data); |
| |
| QLowEnergyHandle attributeHandle = bt_get_le16(&data[0]); |
| |
| // the next 2 elements are not required as we have discovered |
| // all (primary/secondary) services already. Now we are only |
| // interested in their relationship to each other |
| // data[2] -> included service start handle |
| // data[4] -> included service end handle |
| |
| if (elementLength == 8) //16 bit uuid |
| foundServices->append(QBluetoothUuid(bt_get_le16(&data[6]))); |
| else |
| foundServices->append(convert_uuid128((quint128 *) &data[6])); |
| |
| qCDebug(QT_BT_BLUEZ) << "Found included service: " << hex |
| << attributeHandle << "uuid:" << *foundServices; |
| |
| return attributeHandle; |
| } |
| |
| void QLowEnergyControllerPrivateBluez::processReply( |
| const Request &request, const QByteArray &response) |
| { |
| Q_Q(QLowEnergyController); |
| |
| quint8 command = response.constData()[0]; |
| |
| bool isErrorResponse = false; |
| // if error occurred 2. byte is previous request type |
| if (command == ATT_OP_ERROR_RESPONSE) { |
| dumpErrorInformation(response); |
| command = response.constData()[1]; |
| isErrorResponse = true; |
| } |
| |
| switch (command) { |
| case ATT_OP_EXCHANGE_MTU_REQUEST: // in case of error |
| case ATT_OP_EXCHANGE_MTU_RESPONSE: |
| { |
| Q_ASSERT(request.command == ATT_OP_EXCHANGE_MTU_REQUEST); |
| if (isErrorResponse) { |
| mtuSize = ATT_DEFAULT_LE_MTU; |
| break; |
| } |
| |
| const char *data = response.constData(); |
| quint16 mtu = bt_get_le16(&data[1]); |
| mtuSize = mtu; |
| if (mtuSize < ATT_DEFAULT_LE_MTU) |
| mtuSize = ATT_DEFAULT_LE_MTU; |
| |
| qCDebug(QT_BT_BLUEZ) << "Server MTU:" << mtu << "resulting mtu:" << mtuSize; |
| } |
| break; |
| case ATT_OP_READ_BY_GROUP_REQUEST: // in case of error |
| case ATT_OP_READ_BY_GROUP_RESPONSE: |
| { |
| // Discovering services |
| Q_ASSERT(request.command == ATT_OP_READ_BY_GROUP_REQUEST); |
| |
| const quint16 type = request.reference.toUInt(); |
| |
| if (isErrorResponse) { |
| if (type == GATT_SECONDARY_SERVICE) { |
| setState(QLowEnergyController::DiscoveredState); |
| q->discoveryFinished(); |
| } else { // search for secondary services |
| sendReadByGroupRequest(0x0001, 0xFFFF, GATT_SECONDARY_SERVICE); |
| } |
| break; |
| } |
| |
| QLowEnergyHandle start = 0, end = 0; |
| const quint16 elementLength = response.constData()[1]; |
| const quint16 numElements = (response.size() - 2) / elementLength; |
| quint16 offset = 2; |
| const char *data = response.constData(); |
| for (int i = 0; i < numElements; i++) { |
| start = bt_get_le16(&data[offset]); |
| end = bt_get_le16(&data[offset+2]); |
| |
| QBluetoothUuid uuid; |
| if (elementLength == 6) //16 bit uuid |
| uuid = QBluetoothUuid(bt_get_le16(&data[offset+4])); |
| else if (elementLength == 20) //128 bit uuid |
| uuid = convert_uuid128((quint128 *)&data[offset+4]); |
| //else -> do nothing |
| |
| offset += elementLength; |
| |
| |
| qCDebug(QT_BT_BLUEZ) << "Found uuid:" << uuid << "start handle:" << hex |
| << start << "end handle:" << end; |
| |
| QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); |
| priv->uuid = uuid; |
| priv->startHandle = start; |
| priv->endHandle = end; |
| if (type != GATT_PRIMARY_SERVICE) //unset PrimaryService bit |
| priv->type &= ~QLowEnergyService::PrimaryService; |
| priv->setController(this); |
| |
| QSharedPointer<QLowEnergyServicePrivate> pointer(priv); |
| |
| serviceList.insert(uuid, pointer); |
| emit q->serviceDiscovered(uuid); |
| } |
| |
| if (end != 0xFFFF) { |
| sendReadByGroupRequest(end+1, 0xFFFF, type); |
| } else { |
| if (type == GATT_SECONDARY_SERVICE) { |
| setState(QLowEnergyController::DiscoveredState); |
| emit q->discoveryFinished(); |
| } else { // search for secondary services |
| sendReadByGroupRequest(0x0001, 0xFFFF, GATT_SECONDARY_SERVICE); |
| } |
| } |
| } |
| break; |
| case ATT_OP_READ_BY_TYPE_REQUEST: //in case of error |
| case ATT_OP_READ_BY_TYPE_RESPONSE: |
| { |
| // Discovering characteristics |
| Q_ASSERT(request.command == ATT_OP_READ_BY_TYPE_REQUEST); |
| |
| QSharedPointer<QLowEnergyServicePrivate> p = |
| request.reference.value<QSharedPointer<QLowEnergyServicePrivate> >(); |
| const quint16 attributeType = request.reference2.toUInt(); |
| |
| if (isErrorResponse) { |
| if (attributeType == GATT_CHARACTERISTIC) { |
| // we reached end of service handle |
| // just finished up characteristic discovery |
| // continue with values of characteristics |
| if (!p->characteristicList.isEmpty()) { |
| readServiceValues(p->uuid, true); |
| } else { |
| // discovery finished since the service doesn't have any |
| // characteristics |
| p->setState(QLowEnergyService::ServiceDiscovered); |
| } |
| } else if (attributeType == GATT_INCLUDED_SERVICE) { |
| // finished up include discovery |
| // continue with characteristic discovery |
| sendReadByTypeRequest(p, p->startHandle, GATT_CHARACTERISTIC); |
| } |
| break; |
| } |
| |
| /* packet format: |
| * if GATT_CHARACTERISTIC discovery |
| * <opcode><elementLength> |
| * [<handle><property><charHandle><uuid>]+ |
| * |
| * if GATT_INCLUDE discovery |
| * <opcode><elementLength> |
| * [<handle><startHandle_included><endHandle_included><uuid>]+ |
| * |
| * The uuid can be 16 or 128 bit. |
| */ |
| QLowEnergyHandle lastHandle; |
| const quint16 elementLength = response.constData()[1]; |
| const quint16 numElements = (response.size() - 2) / elementLength; |
| quint16 offset = 2; |
| const char *data = response.constData(); |
| for (int i = 0; i < numElements; i++) { |
| if (attributeType == GATT_CHARACTERISTIC) { |
| QLowEnergyServicePrivate::CharData characteristic; |
| lastHandle = parseReadByTypeCharDiscovery( |
| &characteristic, &data[offset], elementLength); |
| p->characteristicList[lastHandle] = characteristic; |
| offset += elementLength; |
| } else if (attributeType == GATT_INCLUDED_SERVICE) { |
| QList<QBluetoothUuid> includedServices; |
| lastHandle = parseReadByTypeIncludeDiscovery( |
| &includedServices, &data[offset], elementLength); |
| p->includedServices = includedServices; |
| for (const QBluetoothUuid &uuid : qAsConst(includedServices)) { |
| if (serviceList.contains(uuid)) |
| serviceList[uuid]->type |= QLowEnergyService::IncludedService; |
| } |
| } |
| } |
| |
| if (lastHandle + 1 < p->endHandle) { // more chars to discover |
| sendReadByTypeRequest(p, lastHandle + 1, attributeType); |
| } else { |
| if (attributeType == GATT_INCLUDED_SERVICE) |
| sendReadByTypeRequest(p, p->startHandle, GATT_CHARACTERISTIC); |
| else |
| readServiceValues(p->uuid, true); |
| } |
| } |
| break; |
| case ATT_OP_READ_REQUEST: //error case |
| case ATT_OP_READ_RESPONSE: |
| { |
| //Reading characteristics and descriptors |
| Q_ASSERT(request.command == ATT_OP_READ_REQUEST); |
| |
| uint handleData = request.reference.toUInt(); |
| const QLowEnergyHandle charHandle = (handleData & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((handleData >> 16) & 0xffff); |
| |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); |
| Q_ASSERT(!service.isNull()); |
| bool isServiceDiscoveryRun |
| = !(service->state == QLowEnergyService::ServiceDiscovered); |
| |
| if (isErrorResponse) { |
| Q_ASSERT(!encryptionChangePending); |
| encryptionChangePending = increaseEncryptLevelfRequired(response.constData()[4]); |
| if (encryptionChangePending) { |
| // Just requested a security level change. |
| // Retry the same command again once the change has happened |
| openRequests.prepend(request); |
| break; |
| } else if (!isServiceDiscoveryRun) { |
| // not encryption problem -> abort readCharacteristic()/readDescriptor() run |
| if (!descriptorHandle) |
| service->setError(QLowEnergyService::CharacteristicReadError); |
| else |
| service->setError(QLowEnergyService::DescriptorReadError); |
| } |
| } else { |
| if (!descriptorHandle) |
| updateValueOfCharacteristic(charHandle, response.mid(1), NEW_VALUE); |
| else |
| updateValueOfDescriptor(charHandle, descriptorHandle, |
| response.mid(1), NEW_VALUE); |
| |
| if (response.size() == mtuSize) { |
| qCDebug(QT_BT_BLUEZ) << "Switching to blob reads for" |
| << charHandle << descriptorHandle |
| << service->characteristicList[charHandle].uuid.toString(); |
| // Potentially more data -> switch to blob reads |
| readServiceValuesByOffset(handleData, mtuSize-1, |
| request.reference2.toBool()); |
| break; |
| } else if (!isServiceDiscoveryRun) { |
| // readCharacteristic() or readDescriptor() ongoing |
| if (!descriptorHandle) { |
| QLowEnergyCharacteristic ch(service, charHandle); |
| emit service->characteristicRead(ch, response.mid(1)); |
| } else { |
| QLowEnergyDescriptor descriptor(service, charHandle, descriptorHandle); |
| emit service->descriptorRead(descriptor, response.mid(1)); |
| } |
| break; |
| } |
| } |
| |
| if (request.reference2.toBool() && isServiceDiscoveryRun) { |
| // we only run into this code path during the initial service discovery |
| // and not when processing readCharacteristics() after service discovery |
| |
| //last characteristic -> progress to descriptor discovery |
| //last descriptor -> service discovery is done |
| if (!descriptorHandle) |
| discoverServiceDescriptors(service->uuid); |
| else |
| service->setState(QLowEnergyService::ServiceDiscovered); |
| } |
| } |
| break; |
| case ATT_OP_READ_BLOB_REQUEST: //error case |
| case ATT_OP_READ_BLOB_RESPONSE: |
| { |
| //Reading characteristic or descriptor with value longer value than MTU |
| Q_ASSERT(request.command == ATT_OP_READ_BLOB_REQUEST); |
| |
| uint handleData = request.reference.toUInt(); |
| const QLowEnergyHandle charHandle = (handleData & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((handleData >> 16) & 0xffff); |
| |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); |
| Q_ASSERT(!service.isNull()); |
| |
| /* |
| * READ_BLOB does not require encryption setup code. BLOB commands |
| * are only issued after read request if the read request is too long |
| * for single MTU. The preceding read request would have triggered |
| * the setup of the encryption already. |
| */ |
| if (!isErrorResponse) { |
| quint16 length = 0; |
| if (!descriptorHandle) |
| length = updateValueOfCharacteristic(charHandle, response.mid(1), APPEND_VALUE); |
| else |
| length = updateValueOfDescriptor(charHandle, descriptorHandle, |
| response.mid(1), APPEND_VALUE); |
| |
| if (response.size() == mtuSize) { |
| readServiceValuesByOffset(handleData, length, |
| request.reference2.toBool()); |
| break; |
| } else if (service->state == QLowEnergyService::ServiceDiscovered) { |
| // readCharacteristic() or readDescriptor() ongoing |
| if (!descriptorHandle) { |
| QLowEnergyCharacteristic ch(service, charHandle); |
| emit service->characteristicRead(ch, ch.value()); |
| } else { |
| QLowEnergyDescriptor descriptor(service, charHandle, descriptorHandle); |
| emit service->descriptorRead(descriptor, descriptor.value()); |
| } |
| break; |
| } |
| } else { |
| qWarning() << "READ BLOB for char:" << charHandle |
| << "descriptor:" << descriptorHandle << "on service" |
| << service->uuid.toString() << "failed (service discovery run:" |
| << (service->state == QLowEnergyService::ServiceDiscovered) << ")"; |
| } |
| |
| if (request.reference2.toBool()) { |
| //last overlong characteristic -> progress to descriptor discovery |
| //last overlong descriptor -> service discovery is done |
| |
| if (!descriptorHandle) |
| discoverServiceDescriptors(service->uuid); |
| else |
| service->setState(QLowEnergyService::ServiceDiscovered); |
| } |
| |
| } |
| break; |
| case ATT_OP_FIND_INFORMATION_REQUEST: //error case |
| case ATT_OP_FIND_INFORMATION_RESPONSE: |
| { |
| //Discovering descriptors |
| Q_ASSERT(request.command == ATT_OP_FIND_INFORMATION_REQUEST); |
| |
| /* packet format: |
| * <opcode><format>[<handle><descriptor_uuid>]+ |
| * |
| * The uuid can be 16 or 128 bit which is indicated by format. |
| */ |
| |
| QList<QLowEnergyHandle> keys = request.reference.value<QList<QLowEnergyHandle> >(); |
| if (keys.isEmpty()) { |
| qCWarning(QT_BT_BLUEZ) << "Descriptor discovery for unknown characteristic received"; |
| break; |
| } |
| QLowEnergyHandle charHandle = keys.first(); |
| |
| QSharedPointer<QLowEnergyServicePrivate> p = |
| serviceForHandle(charHandle); |
| Q_ASSERT(!p.isNull()); |
| |
| if (isErrorResponse) { |
| if (keys.count() == 1) { |
| // no more descriptors to discover |
| readServiceValues(p->uuid, false); //read descriptor values |
| } else { |
| // hop to the next descriptor |
| keys.removeFirst(); |
| discoverNextDescriptor(p, keys, keys.first()); |
| } |
| break; |
| } |
| |
| const quint8 format = response[1]; |
| quint16 elementLength; |
| switch (format) { |
| case 0x01: |
| elementLength = 2 + 2; //sizeof(QLowEnergyHandle) + 16bit uuid |
| break; |
| case 0x02: |
| elementLength = 2 + 16; //sizeof(QLowEnergyHandle) + 128bit uuid |
| break; |
| default: |
| qCWarning(QT_BT_BLUEZ) << "Unknown format in FIND_INFORMATION_RESPONSE"; |
| return; |
| } |
| |
| const quint16 numElements = (response.size() - 2) / elementLength; |
| |
| quint16 offset = 2; |
| QLowEnergyHandle descriptorHandle; |
| QBluetoothUuid uuid; |
| const char *data = response.constData(); |
| for (int i = 0; i < numElements; i++) { |
| descriptorHandle = bt_get_le16(&data[offset]); |
| |
| if (format == 0x01) |
| uuid = QBluetoothUuid(bt_get_le16(&data[offset+2])); |
| else if (format == 0x02) |
| uuid = convert_uuid128((quint128 *)&data[offset+2]); |
| |
| offset += elementLength; |
| |
| // ignore all attributes which are not of type descriptor |
| // examples are the characteristics value or |
| bool ok = false; |
| quint16 shortUuid = uuid.toUInt16(&ok); |
| if (ok && shortUuid >= QLowEnergyServicePrivate::PrimaryService |
| && shortUuid <= QLowEnergyServicePrivate::Characteristic){ |
| qCDebug(QT_BT_BLUEZ) << "Suppressing primary/characteristic" << hex << shortUuid; |
| continue; |
| } |
| |
| // ignore value handle |
| if (descriptorHandle == p->characteristicList[charHandle].valueHandle) { |
| qCDebug(QT_BT_BLUEZ) << "Suppressing char handle" << hex << descriptorHandle; |
| continue; |
| } |
| |
| QLowEnergyServicePrivate::DescData data; |
| data.uuid = uuid; |
| p->characteristicList[charHandle].descriptorList.insert( |
| descriptorHandle, data); |
| |
| qCDebug(QT_BT_BLUEZ) << "Descriptor found, uuid:" |
| << uuid.toString() |
| << "descriptor handle:" << hex << descriptorHandle; |
| } |
| |
| const QLowEnergyHandle nextPotentialHandle = descriptorHandle + 1; |
| if (keys.count() == 1) { |
| // Reached last characteristic of service |
| |
| // The endhandle of a service is always the last handle of |
| // the current service. We must either continue until we have reached |
| // the starting handle of the next service (endHandle+1) or |
| // the last physical handle address (0xffff). Note that |
| // the endHandle of the last service on the device is 0xffff. |
| |
| if ((p->endHandle != 0xffff && nextPotentialHandle >= p->endHandle + 1) |
| || (descriptorHandle == 0xffff)) { |
| keys.removeFirst(); |
| // last descriptor of last characteristic found |
| // continue with reading descriptor values |
| readServiceValues(p->uuid, false); |
| } else { |
| discoverNextDescriptor(p, keys, nextPotentialHandle); |
| } |
| } else { |
| if (nextPotentialHandle >= keys[1]) //reached next char |
| keys.removeFirst(); |
| discoverNextDescriptor(p, keys, nextPotentialHandle); |
| } |
| } |
| break; |
| case ATT_OP_WRITE_REQUEST: //error case |
| case ATT_OP_WRITE_RESPONSE: |
| { |
| //Write command response |
| Q_ASSERT(request.command == ATT_OP_WRITE_REQUEST); |
| |
| uint ref = request.reference.toUInt(); |
| const QLowEnergyHandle charHandle = (ref & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((ref >> 16) & 0xffff); |
| |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); |
| if (service.isNull() || !service->characteristicList.contains(charHandle)) |
| break; |
| |
| if (isErrorResponse) { |
| Q_ASSERT(!encryptionChangePending); |
| encryptionChangePending = increaseEncryptLevelfRequired(response.constData()[4]); |
| if (encryptionChangePending) { |
| openRequests.prepend(request); |
| break; |
| } |
| |
| if (!descriptorHandle) |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| else |
| service->setError(QLowEnergyService::DescriptorWriteError); |
| break; |
| } |
| |
| const QByteArray newValue = request.reference2.toByteArray(); |
| if (!descriptorHandle) { |
| QLowEnergyCharacteristic ch(service, charHandle); |
| if (ch.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(charHandle, newValue, NEW_VALUE); |
| emit service->characteristicWritten(ch, newValue); |
| } else { |
| updateValueOfDescriptor(charHandle, descriptorHandle, newValue, NEW_VALUE); |
| QLowEnergyDescriptor descriptor(service, charHandle, descriptorHandle); |
| emit service->descriptorWritten(descriptor, newValue); |
| } |
| } |
| break; |
| case ATT_OP_PREPARE_WRITE_REQUEST: //error case |
| case ATT_OP_PREPARE_WRITE_RESPONSE: |
| { |
| //Prepare write command response |
| Q_ASSERT(request.command == ATT_OP_PREPARE_WRITE_REQUEST); |
| |
| uint handleData = request.reference.toUInt(); |
| const QLowEnergyHandle attrHandle = (handleData & 0xffff); |
| const QByteArray newValue = request.reference2.toByteArray(); |
| const int writtenPayload = ((handleData >> 16) & 0xffff); |
| |
| if (isErrorResponse) { |
| Q_ASSERT(!encryptionChangePending); |
| encryptionChangePending = increaseEncryptLevelfRequired(response.constData()[4]); |
| if (encryptionChangePending) { |
| openRequests.prepend(request); |
| break; |
| } |
| //emits error on cancellation and aborts existing prepare reuqests |
| sendExecuteWriteRequest(attrHandle, newValue, true); |
| } else { |
| if (writtenPayload < newValue.size()) { |
| sendNextPrepareWriteRequest(attrHandle, newValue, writtenPayload); |
| } else { |
| sendExecuteWriteRequest(attrHandle, newValue, false); |
| } |
| } |
| } |
| break; |
| case ATT_OP_EXECUTE_WRITE_REQUEST: //error case |
| case ATT_OP_EXECUTE_WRITE_RESPONSE: |
| { |
| // right now used in connection with long characteristic/descriptor value writes |
| // not catering for reliable writes |
| Q_ASSERT(request.command == ATT_OP_EXECUTE_WRITE_REQUEST); |
| |
| uint handleData = request.reference.toUInt(); |
| const QLowEnergyHandle attrHandle = handleData & 0xffff; |
| bool wasCancellation = !((handleData >> 16) & 0xffff); |
| const QByteArray newValue = request.reference2.toByteArray(); |
| |
| // is it a descriptor or characteristic? |
| const QLowEnergyDescriptor descriptor = descriptorForHandle(attrHandle); |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(attrHandle); |
| Q_ASSERT(!service.isNull()); |
| |
| if (isErrorResponse || wasCancellation) { |
| // charHandle == 0 -> cancellation |
| if (descriptor.isValid()) |
| service->setError(QLowEnergyService::DescriptorWriteError); |
| else |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| } else { |
| if (descriptor.isValid()) { |
| updateValueOfDescriptor(descriptor.characteristicHandle(), |
| attrHandle, newValue, NEW_VALUE); |
| emit service->descriptorWritten(descriptor, newValue); |
| } else { |
| QLowEnergyCharacteristic ch(service, attrHandle); |
| if (ch.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(attrHandle, newValue, NEW_VALUE); |
| emit service->characteristicWritten(ch, newValue); |
| } |
| } |
| } |
| break; |
| default: |
| qCDebug(QT_BT_BLUEZ) << "Unknown packet: " << response.toHex(); |
| break; |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::discoverServices() |
| { |
| sendReadByGroupRequest(0x0001, 0xFFFF, GATT_PRIMARY_SERVICE); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendReadByGroupRequest( |
| QLowEnergyHandle start, QLowEnergyHandle end, quint16 type) |
| { |
| //call for primary and secondary services |
| quint8 packet[GRP_TYPE_REQ_HEADER_SIZE]; |
| |
| packet[0] = ATT_OP_READ_BY_GROUP_REQUEST; |
| 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); |
| qCDebug(QT_BT_BLUEZ) << "Sending read_by_group_type request, startHandle:" << hex |
| << start << "endHandle:" << end << type; |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_BY_GROUP_REQUEST; |
| request.reference = type; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::discoverServiceDetails(const QBluetoothUuid &service) |
| { |
| if (!serviceList.contains(service)) { |
| qCWarning(QT_BT_BLUEZ) << "Discovery of unknown service" << service.toString() |
| << "not possible"; |
| return; |
| } |
| |
| QSharedPointer<QLowEnergyServicePrivate> serviceData = serviceList.value(service); |
| serviceData->characteristicList.clear(); |
| sendReadByTypeRequest(serviceData, serviceData->startHandle, GATT_INCLUDED_SERVICE); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendReadByTypeRequest( |
| QSharedPointer<QLowEnergyServicePrivate> serviceData, |
| QLowEnergyHandle nextHandle, quint16 attributeType) |
| { |
| quint8 packet[READ_BY_TYPE_REQ_HEADER_SIZE]; |
| |
| packet[0] = ATT_OP_READ_BY_TYPE_REQUEST; |
| 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); |
| qCDebug(QT_BT_BLUEZ) << "Sending read_by_type request, startHandle:" << hex |
| << nextHandle << "endHandle:" << serviceData->endHandle |
| << "type:" << attributeType << "packet:" << data.toHex(); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_BY_TYPE_REQUEST; |
| request.reference = QVariant::fromValue(serviceData); |
| request.reference2 = attributeType; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| /*! |
| \internal |
| |
| Reads all values of specific characteristic and descriptor. This function is |
| used during the initial service discovery process. |
| |
| \a readCharacteristics determines whether we intend to read a characteristic; |
| otherwise we read a descriptor. |
| */ |
| void QLowEnergyControllerPrivateBluez::readServiceValues( |
| const QBluetoothUuid &serviceUuid, bool readCharacteristics) |
| { |
| quint8 packet[READ_REQUEST_HEADER_SIZE]; |
| if (QT_BT_BLUEZ().isDebugEnabled()) { |
| if (readCharacteristics) |
| qCDebug(QT_BT_BLUEZ) << "Reading all characteristic values for" |
| << serviceUuid.toString(); |
| else |
| qCDebug(QT_BT_BLUEZ) << "Reading all descriptor values for" |
| << serviceUuid.toString(); |
| } |
| |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceList.value(serviceUuid); |
| |
| // pair.first -> target attribute |
| // pair.second -> context information for read request |
| QPair<QLowEnergyHandle, quint32> pair; |
| |
| // Create list of attribute handles which need to be read |
| QList<QPair<QLowEnergyHandle, quint32> > targetHandles; |
| |
| CharacteristicDataMap::const_iterator charIt = service->characteristicList.constBegin(); |
| for ( ; charIt != service->characteristicList.constEnd(); ++charIt) { |
| const QLowEnergyHandle charHandle = charIt.key(); |
| const QLowEnergyServicePrivate::CharData &charDetails = charIt.value(); |
| |
| if (readCharacteristics) { |
| // Collect handles of all characteristic value attributes |
| |
| // Don't try to read writeOnly characteristic |
| if (!(charDetails.properties & QLowEnergyCharacteristic::Read)) |
| continue; |
| |
| pair.first = charDetails.valueHandle; |
| pair.second = charHandle; |
| targetHandles.append(pair); |
| |
| } else { |
| // Collect handles of all descriptor attributes |
| DescriptorDataMap::const_iterator descIt = charDetails.descriptorList.constBegin(); |
| for ( ; descIt != charDetails.descriptorList.constEnd(); ++descIt) { |
| const QLowEnergyHandle descriptorHandle = descIt.key(); |
| |
| pair.first = descriptorHandle; |
| pair.second = (charHandle | (descriptorHandle << 16)); |
| targetHandles.append(pair); |
| } |
| } |
| } |
| |
| |
| if (targetHandles.isEmpty()) { |
| if (readCharacteristics) { |
| // none of the characteristics is readable |
| // -> continue with descriptor discovery |
| discoverServiceDescriptors(service->uuid); |
| } else { |
| // characteristic w/o descriptors |
| service->setState(QLowEnergyService::ServiceDiscovered); |
| } |
| return; |
| } |
| |
| for (int i = 0; i < targetHandles.count(); i++) { |
| pair = targetHandles.at(i); |
| packet[0] = ATT_OP_READ_REQUEST; |
| putBtData(pair.first, &packet[1]); |
| |
| QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); |
| memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_REQUEST; |
| request.reference = pair.second; |
| // last entry? |
| request.reference2 = QVariant((bool)(i + 1 == targetHandles.count())); |
| openRequests.enqueue(request); |
| } |
| |
| sendNextPendingRequest(); |
| } |
| |
| /*! |
| \internal |
| |
| This function is used when reading a handle value that is |
| longer than the mtuSize. |
| |
| The BLOB read request is prepended to the list of |
| open requests to finish the current value read up before |
| starting the next read request. |
| */ |
| void QLowEnergyControllerPrivateBluez::readServiceValuesByOffset( |
| uint handleData, quint16 offset, bool isLastValue) |
| { |
| const QLowEnergyHandle charHandle = (handleData & 0xffff); |
| const QLowEnergyHandle descriptorHandle = ((handleData >> 16) & 0xffff); |
| |
| QByteArray data(READ_BLOB_REQUEST_HEADER_SIZE, Qt::Uninitialized); |
| data[0] = ATT_OP_READ_BLOB_REQUEST; |
| |
| QLowEnergyHandle handleToRead = charHandle; |
| if (descriptorHandle) { |
| handleToRead = descriptorHandle; |
| qCDebug(QT_BT_BLUEZ) << "Reading descriptor via blob request" |
| << hex << descriptorHandle; |
| } else { |
| //charHandle is not the char's value handle |
| QSharedPointer<QLowEnergyServicePrivate> service = |
| serviceForHandle(charHandle); |
| if (!service.isNull() |
| && service->characteristicList.contains(charHandle)) { |
| handleToRead = service->characteristicList[charHandle].valueHandle; |
| qCDebug(QT_BT_BLUEZ) << "Reading characteristic via blob request" |
| << hex << handleToRead; |
| } else { |
| Q_ASSERT(false); |
| } |
| } |
| |
| putBtData(handleToRead, data.data() + 1); |
| putBtData(offset, data.data() + 3); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_BLOB_REQUEST; |
| request.reference = handleData; |
| request.reference2 = isLastValue; |
| openRequests.prepend(request); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::discoverServiceDescriptors( |
| const QBluetoothUuid &serviceUuid) |
| { |
| qCDebug(QT_BT_BLUEZ) << "Discovering descriptor values for" |
| << serviceUuid.toString(); |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceList.value(serviceUuid); |
| |
| if (service->characteristicList.isEmpty()) { // service has no characteristics |
| // implies that characteristic & descriptor discovery can be skipped |
| service->setState(QLowEnergyService::ServiceDiscovered); |
| return; |
| } |
| |
| // start handle of all known characteristics |
| QList<QLowEnergyHandle> keys = service->characteristicList.keys(); |
| std::sort(keys.begin(), keys.end()); |
| |
| discoverNextDescriptor(service, keys, keys[0]); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::processUnsolicitedReply(const QByteArray &payload) |
| { |
| const char *data = payload.constData(); |
| bool isNotification = (data[0] == ATT_OP_HANDLE_VAL_NOTIFICATION); |
| const QLowEnergyHandle changedHandle = bt_get_le16(&data[1]); |
| |
| if (QT_BT_BLUEZ().isDebugEnabled()) { |
| if (isNotification) |
| qCDebug(QT_BT_BLUEZ) << "Change notification for handle" << hex << changedHandle; |
| else |
| qCDebug(QT_BT_BLUEZ) << "Change indication for handle" << hex << changedHandle; |
| } |
| |
| const QLowEnergyCharacteristic ch = characteristicForHandle(changedHandle); |
| if (ch.isValid() && ch.handle() == changedHandle) { |
| if (ch.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(ch.attributeHandle(), payload.mid(3), NEW_VALUE); |
| emit ch.d_ptr->characteristicChanged(ch, payload.mid(3)); |
| } else { |
| qCWarning(QT_BT_BLUEZ) << "Cannot find matching characteristic for " |
| "notification/indication"; |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::exchangeMTU() |
| { |
| qCDebug(QT_BT_BLUEZ) << "Exchanging MTU"; |
| |
| quint8 packet[MTU_EXCHANGE_HEADER_SIZE]; |
| packet[0] = ATT_OP_EXCHANGE_MTU_REQUEST; |
| putBtData(quint16(ATT_MAX_LE_MTU), &packet[1]); |
| |
| QByteArray data(MTU_EXCHANGE_HEADER_SIZE, Qt::Uninitialized); |
| memcpy(data.data(), packet, MTU_EXCHANGE_HEADER_SIZE); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_EXCHANGE_MTU_REQUEST; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| int QLowEnergyControllerPrivateBluez::securityLevel() const |
| { |
| int socket = l2cpSocket->socketDescriptor(); |
| if (socket < 0) { |
| qCWarning(QT_BT_BLUEZ) << "Invalid l2cp socket, aborting getting of sec level"; |
| return -1; |
| } |
| |
| struct bt_security secData; |
| socklen_t length = sizeof(secData); |
| memset(&secData, 0, length); |
| |
| if (getsockopt(socket, SOL_BLUETOOTH, BT_SECURITY, &secData, &length) == 0) { |
| qCDebug(QT_BT_BLUEZ) << "Current l2cp sec level:" << secData.level; |
| return secData.level; |
| } |
| |
| if (errno != ENOPROTOOPT) //older kernel, fall back to L2CAP_LM option |
| return -1; |
| |
| // cater for older kernels |
| int optval; |
| length = sizeof(optval); |
| if (getsockopt(socket, SOL_L2CAP, L2CAP_LM, &optval, &length) == 0) { |
| int level = BT_SECURITY_SDP; |
| if (optval & L2CAP_LM_AUTH) |
| level = BT_SECURITY_LOW; |
| if (optval & L2CAP_LM_ENCRYPT) |
| level = BT_SECURITY_MEDIUM; |
| if (optval & L2CAP_LM_SECURE) |
| level = BT_SECURITY_HIGH; |
| |
| qDebug() << "Current l2cp sec level (old):" << level; |
| return level; |
| } |
| |
| return -1; |
| } |
| |
| bool QLowEnergyControllerPrivateBluez::setSecurityLevel(int level) |
| { |
| if (level > BT_SECURITY_HIGH || level < BT_SECURITY_LOW) |
| return false; |
| |
| int socket = l2cpSocket->socketDescriptor(); |
| if (socket < 0) { |
| qCWarning(QT_BT_BLUEZ) << "Invalid l2cp socket, aborting setting of sec level"; |
| return false; |
| } |
| |
| struct bt_security secData; |
| socklen_t length = sizeof(secData); |
| memset(&secData, 0, length); |
| secData.level = level; |
| |
| if (setsockopt(socket, SOL_BLUETOOTH, BT_SECURITY, &secData, length) == 0) { |
| qCDebug(QT_BT_BLUEZ) << "Setting new l2cp sec level:" << secData.level; |
| return true; |
| } |
| |
| if (errno != ENOPROTOOPT) //older kernel |
| return false; |
| |
| int optval = 0; |
| switch (level) { // fall through intendeds |
| case BT_SECURITY_HIGH: |
| optval |= L2CAP_LM_SECURE; |
| Q_FALLTHROUGH(); |
| case BT_SECURITY_MEDIUM: |
| optval |= L2CAP_LM_ENCRYPT; |
| Q_FALLTHROUGH(); |
| case BT_SECURITY_LOW: |
| optval |= L2CAP_LM_AUTH; |
| break; |
| default: |
| return false; |
| } |
| |
| if (setsockopt(socket, SOL_L2CAP, L2CAP_LM, &optval, sizeof(optval)) == 0) { |
| qDebug(QT_BT_BLUEZ) << "Old l2cp sec level:" << optval; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void QLowEnergyControllerPrivateBluez::discoverNextDescriptor( |
| QSharedPointer<QLowEnergyServicePrivate> serviceData, |
| const QList<QLowEnergyHandle> pendingCharHandles, |
| const QLowEnergyHandle startingHandle) |
| { |
| Q_ASSERT(!pendingCharHandles.isEmpty()); |
| Q_ASSERT(!serviceData.isNull()); |
| |
| qCDebug(QT_BT_BLUEZ) << "Sending find_info request" << hex |
| << pendingCharHandles << startingHandle; |
| |
| quint8 packet[FIND_INFO_REQUEST_HEADER_SIZE]; |
| packet[0] = ATT_OP_FIND_INFORMATION_REQUEST; |
| |
| const QLowEnergyHandle charStartHandle = startingHandle; |
| QLowEnergyHandle charEndHandle = 0; |
| if (pendingCharHandles.count() == 1) //single characteristic |
| charEndHandle = serviceData->endHandle; |
| else |
| charEndHandle = pendingCharHandles[1] - 1; |
| |
| 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); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_FIND_INFORMATION_REQUEST; |
| request.reference = QVariant::fromValue<QList<QLowEnergyHandle> >(pendingCharHandles); |
| request.reference2 = startingHandle; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendNextPrepareWriteRequest( |
| const QLowEnergyHandle handle, const QByteArray &newValue, |
| quint16 offset) |
| { |
| // is it a descriptor or characteristic? |
| QLowEnergyHandle targetHandle = 0; |
| const QLowEnergyDescriptor descriptor = descriptorForHandle(handle); |
| if (descriptor.isValid()) |
| targetHandle = descriptor.handle(); |
| else |
| targetHandle = characteristicForHandle(handle).handle(); |
| |
| if (!targetHandle) { |
| qCWarning(QT_BT_BLUEZ) << "sendNextPrepareWriteRequest cancelled due to invalid handle" |
| << handle; |
| return; |
| } |
| |
| quint8 packet[PREPARE_WRITE_HEADER_SIZE]; |
| packet[0] = ATT_OP_PREPARE_WRITE_REQUEST; |
| putBtData(targetHandle, &packet[1]); // attribute handle |
| putBtData(offset, &packet[3]); // offset into newValue |
| |
| qCDebug(QT_BT_BLUEZ) << "Writing long characteristic (prepare):" |
| << hex << handle; |
| |
| |
| const int maxAvailablePayload = mtuSize - PREPARE_WRITE_HEADER_SIZE; |
| const int requiredPayload = qMin(newValue.size() - offset, maxAvailablePayload); |
| const int dataSize = PREPARE_WRITE_HEADER_SIZE + requiredPayload; |
| |
| Q_ASSERT((offset + requiredPayload) <= newValue.size()); |
| Q_ASSERT(dataSize <= mtuSize); |
| |
| QByteArray data(dataSize, Qt::Uninitialized); |
| memcpy(data.data(), packet, PREPARE_WRITE_HEADER_SIZE); |
| memcpy(&(data.data()[PREPARE_WRITE_HEADER_SIZE]), &(newValue.constData()[offset]), |
| requiredPayload); |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_PREPARE_WRITE_REQUEST; |
| request.reference = (handle | ((offset + requiredPayload) << 16)); |
| request.reference2 = newValue; |
| openRequests.enqueue(request); |
| } |
| |
| /*! |
| Sends an "Execute Write Request" for a long characteristic or descriptor write. |
| This cannot be used for executes in relation to reliable write requests. |
| |
| A cancellation removes all pending prepare write request on the GATT server. |
| Otherwise this function sends an execute request for all pending prepare |
| write requests. |
| */ |
| void QLowEnergyControllerPrivateBluez::sendExecuteWriteRequest( |
| const QLowEnergyHandle attrHandle, const QByteArray &newValue, |
| bool isCancelation) |
| { |
| quint8 packet[EXECUTE_WRITE_HEADER_SIZE]; |
| packet[0] = ATT_OP_EXECUTE_WRITE_REQUEST; |
| if (isCancelation) |
| packet[1] = 0x00; // cancel pending write prepare requests |
| else |
| packet[1] = 0x01; // execute pending write prepare requests |
| |
| QByteArray data(EXECUTE_WRITE_HEADER_SIZE, Qt::Uninitialized); |
| memcpy(data.data(), packet, EXECUTE_WRITE_HEADER_SIZE); |
| |
| qCDebug(QT_BT_BLUEZ) << "Sending Execute Write Request for long characteristic value" |
| << hex << attrHandle; |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_EXECUTE_WRITE_REQUEST; |
| request.reference = (attrHandle | ((isCancelation ? 0x00 : 0x01) << 16)); |
| request.reference2 = newValue; |
| openRequests.prepend(request); |
| } |
| |
| |
| /*! |
| Writes long (prepare write request), short (write request) |
| and writeWithoutResponse characteristic values. |
| |
| TODO Reliable/prepare write across multiple characteristics is not supported |
| */ |
| void QLowEnergyControllerPrivateBluez::writeCharacteristic( |
| const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, |
| const QByteArray &newValue, |
| QLowEnergyService::WriteMode mode) |
| { |
| Q_ASSERT(!service.isNull()); |
| |
| if (!service->characteristicList.contains(charHandle)) |
| return; |
| |
| QLowEnergyServicePrivate::CharData &charData = service->characteristicList[charHandle]; |
| if (role == QLowEnergyController::PeripheralRole) |
| writeCharacteristicForPeripheral(charData, newValue); |
| else |
| writeCharacteristicForCentral(service, charHandle, charData.valueHandle, newValue, mode); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::writeDescriptor( |
| const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, |
| const QLowEnergyHandle descriptorHandle, |
| const QByteArray &newValue) |
| { |
| Q_ASSERT(!service.isNull()); |
| |
| if (role == QLowEnergyController::PeripheralRole) |
| writeDescriptorForPeripheral(service, charHandle, descriptorHandle, newValue); |
| else |
| writeDescriptorForCentral(charHandle, descriptorHandle, newValue); |
| } |
| |
| /*! |
| \internal |
| |
| Reads the value of one specific characteristic. |
| */ |
| void QLowEnergyControllerPrivateBluez::readCharacteristic( |
| const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle) |
| { |
| Q_ASSERT(!service.isNull()); |
| if (!service->characteristicList.contains(charHandle)) |
| return; |
| |
| const QLowEnergyServicePrivate::CharData &charDetails |
| = service->characteristicList[charHandle]; |
| if (!(charDetails.properties & QLowEnergyCharacteristic::Read)) { |
| // if this succeeds the device has a bug, char is advertised as |
| // non-readable. We try to be permissive and let the remote |
| // device answer to the read attempt |
| qCWarning(QT_BT_BLUEZ) << "Reading non-readable char" << charHandle; |
| } |
| |
| quint8 packet[READ_REQUEST_HEADER_SIZE]; |
| packet[0] = ATT_OP_READ_REQUEST; |
| putBtData(charDetails.valueHandle, &packet[1]); |
| |
| QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); |
| memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); |
| |
| qCDebug(QT_BT_BLUEZ) << "Targeted reading characteristic" << hex << charHandle; |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_REQUEST; |
| request.reference = charHandle; |
| // reference2 not really required but false prevents service discovery |
| // code from running in ATT_OP_READ_RESPONSE handler |
| request.reference2 = false; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::readDescriptor( |
| const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, |
| const QLowEnergyHandle descriptorHandle) |
| { |
| Q_ASSERT(!service.isNull()); |
| if (!service->characteristicList.contains(charHandle)) |
| return; |
| |
| const QLowEnergyServicePrivate::CharData &charDetails |
| = service->characteristicList[charHandle]; |
| if (!charDetails.descriptorList.contains(descriptorHandle)) |
| return; |
| |
| quint8 packet[READ_REQUEST_HEADER_SIZE]; |
| packet[0] = ATT_OP_READ_REQUEST; |
| putBtData(descriptorHandle, &packet[1]); |
| |
| QByteArray data(READ_REQUEST_HEADER_SIZE, Qt::Uninitialized); |
| memcpy(data.data(), packet, READ_REQUEST_HEADER_SIZE); |
| |
| qCDebug(QT_BT_BLUEZ) << "Targeted reading descriptor" << hex << descriptorHandle; |
| |
| Request request; |
| request.payload = data; |
| request.command = ATT_OP_READ_REQUEST; |
| request.reference = (charHandle | (descriptorHandle << 16)); |
| // reference2 not really required but false prevents service discovery |
| // code from running in ATT_OP_READ_RESPONSE handler |
| request.reference2 = false; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| /*! |
| * Returns true if the encryption change was successfully requested. |
| * The request is triggered if we got a related ATT error. |
| */ |
| bool QLowEnergyControllerPrivateBluez::increaseEncryptLevelfRequired(quint8 errorCode) |
| { |
| if (securityLevelValue == BT_SECURITY_HIGH) |
| return false; |
| |
| switch (errorCode) { |
| case ATT_ERROR_INSUF_ENCRYPTION: |
| case ATT_ERROR_INSUF_AUTHENTICATION: |
| case ATT_ERROR_INSUF_ENCR_KEY_SIZE: |
| if (!hciManager->isValid()) |
| return false; |
| if (!hciManager->monitorEvent(HciManager::EncryptChangeEvent)) |
| return false; |
| if (securityLevelValue != BT_SECURITY_HIGH) { |
| qCDebug(QT_BT_BLUEZ) << "Requesting encrypted link"; |
| if (setSecurityLevel(BT_SECURITY_HIGH)) { |
| restartRequestTimer(); |
| return true; |
| } |
| } |
| break; |
| default: |
| break; |
| } |
| |
| return false; |
| } |
| |
| void QLowEnergyControllerPrivateBluez::handleAdvertisingError() |
| { |
| qCWarning(QT_BT_BLUEZ) << "received advertising error"; |
| setError(QLowEnergyController::AdvertisingError); |
| setState(QLowEnergyController::UnconnectedState); |
| } |
| |
| bool QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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); |
| for (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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::updateLocalAttributeValue( |
| QLowEnergyHandle handle, |
| const QByteArray &value, |
| QLowEnergyCharacteristic &characteristic, |
| QLowEnergyDescriptor &descriptor) |
| { |
| localAttributes[handle].value = value; |
| for (const auto &service : qAsConst(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 QLowEnergyControllerPrivateBluez::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; |
| for (const QLowEnergyServicePrivate::DescData &desc : qAsConst(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 QLowEnergyControllerPrivateBluez::writeCharacteristicForCentral(const QSharedPointer<QLowEnergyServicePrivate> &service, |
| QLowEnergyHandle charHandle, |
| QLowEnergyHandle valueHandle, |
| const QByteArray &newValue, |
| QLowEnergyService::WriteMode mode) |
| { |
| QByteArray packet(WRITE_REQUEST_HEADER_SIZE + newValue.count(), Qt::Uninitialized); |
| putBtData(valueHandle, packet.data() + 1); |
| memcpy(packet.data() + 3, newValue.constData(), newValue.count()); |
| bool writeWithResponse = false; |
| switch (mode) { |
| case QLowEnergyService::WriteWithResponse: |
| if (newValue.size() > (mtuSize - WRITE_REQUEST_HEADER_SIZE)) { |
| sendNextPrepareWriteRequest(charHandle, newValue, 0); |
| sendNextPendingRequest(); |
| return; |
| } |
| // write value fits into single package |
| packet[0] = ATT_OP_WRITE_REQUEST; |
| writeWithResponse = true; |
| break; |
| case QLowEnergyService::WriteWithoutResponse: |
| packet[0] = ATT_OP_WRITE_COMMAND; |
| break; |
| case QLowEnergyService::WriteSigned: |
| packet[0] = ATT_OP_SIGNED_WRITE_COMMAND; |
| if (!isBonded()) { |
| qCWarning(QT_BT_BLUEZ) << "signed write not possible: requires bond between devices"; |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| return; |
| } |
| if (securityLevel() >= BT_SECURITY_MEDIUM) { |
| qCWarning(QT_BT_BLUEZ) << "signed write not possible: not allowed on encrypted link"; |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| return; |
| } |
| const auto signingDataIt = signingData.find(remoteDevice.toUInt64()); |
| if (signingDataIt == signingData.end()) { |
| qCWarning(QT_BT_BLUEZ) << "signed write not possible: no signature key found"; |
| service->setError(QLowEnergyService::CharacteristicWriteError); |
| return; |
| } |
| ++signingDataIt.value().counter; |
| packet = LeCmacCalculator::createFullMessage(packet, signingDataIt.value().counter); |
| const quint64 mac = LeCmacCalculator().calculateMac(packet, signingDataIt.value().key); |
| packet.resize(packet.count() + sizeof mac); |
| putBtData(mac, packet.data() + packet.count() - sizeof mac); |
| storeSignCounter(LocalSigningKey); |
| break; |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << "Writing characteristic" << hex << charHandle |
| << "(size:" << packet.count() << "with response:" |
| << (mode == QLowEnergyService::WriteWithResponse) |
| << "signed:" << (mode == QLowEnergyService::WriteSigned) << ")"; |
| |
| // Advantage of write without response is the quick turnaround. |
| // It can be sent at any time and does not produce responses. |
| // Therefore we will not put them into the openRequest queue at all. |
| if (!writeWithResponse) { |
| sendPacket(packet); |
| return; |
| } |
| |
| Request request; |
| request.payload = packet; |
| request.command = ATT_OP_WRITE_REQUEST; |
| request.reference = charHandle; |
| request.reference2 = newValue; |
| openRequests.enqueue(request); |
| |
| sendNextPendingRequest(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::handleWriteRequestOrCommand(const QByteArray &packet) |
| { |
| // Spec v4.2, Vol 3, Part F, 3.4.5.1-3 |
| |
| const bool isRequest = packet.at(0) == ATT_OP_WRITE_REQUEST; |
| const bool isSigned = quint8(packet.at(0)) == quint8(ATT_OP_SIGNED_WRITE_COMMAND); |
| if (!checkPacketSize(packet, isSigned ? 15 : 3, mtuSize)) |
| return; |
| const QLowEnergyHandle handle = bt_get_le16(packet.constData() + 1); |
| qCDebug(QT_BT_BLUEZ) << "client sends" << (isSigned ? "signed" : "") << "write" |
| << (isRequest ? "request" : "command") << "for handle" << handle; |
| |
| if (!checkHandle(packet, handle)) |
| return; |
| |
| Attribute &attribute = localAttributes[handle]; |
| const QLowEnergyCharacteristic::PropertyType type = isRequest |
| ? QLowEnergyCharacteristic::Write : isSigned |
| ? QLowEnergyCharacteristic::WriteSigned : QLowEnergyCharacteristic::WriteNoResponse; |
| const int permissionsError = checkPermissions(attribute, type); |
| if (permissionsError) { |
| sendErrorResponse(packet.at(0), handle, permissionsError); |
| return; |
| } |
| |
| int valueLength; |
| if (isSigned) { |
| if (!isBonded()) { |
| qCWarning(QT_BT_BLUEZ) << "Ignoring signed write from non-bonded device."; |
| return; |
| } |
| if (securityLevel() >= BT_SECURITY_MEDIUM) { |
| qCWarning(QT_BT_BLUEZ) << "Ignoring signed write on encrypted link."; |
| return; |
| } |
| const auto signingDataIt = signingData.find(remoteDevice.toUInt64()); |
| if (signingDataIt == signingData.constEnd()) { |
| qCWarning(QT_BT_BLUEZ) << "No CSRK found for peer device, ignoring signed write"; |
| return; |
| } |
| |
| const quint32 signCounter = getBtData<quint32>(packet.data() + packet.count() - 12); |
| if (signCounter < signingDataIt.value().counter + 1) { |
| qCWarning(QT_BT_BLUEZ) << "Client's' sign counter" << signCounter |
| << "not greater than local sign counter" |
| << signingDataIt.value().counter |
| << "; ignoring signed write command."; |
| return; |
| } |
| |
| const quint64 macFromClient = getBtData<quint64>(packet.data() + packet.count() - 8); |
| const bool signatureCorrect = verifyMac(packet.left(packet.count() - 12), |
| signingDataIt.value().key, signCounter, macFromClient); |
| if (!signatureCorrect) { |
| qCWarning(QT_BT_BLUEZ) << "Signed Write packet has wrong signature, disconnecting"; |
| disconnectFromDevice(); // Recommended by spec v4.2, Vol 3, part C, 10.4.2 |
| return; |
| } |
| |
| signingDataIt.value().counter = signCounter; |
| storeSignCounter(RemoteSigningKey); |
| valueLength = packet.count() - 15; |
| } else { |
| valueLength = packet.count() - 3; |
| } |
| |
| if (valueLength > attribute.maxLength) { |
| sendErrorResponse(packet.at(0), handle, ATT_ERROR_INVAL_ATTR_VALUE_LEN); |
| return; |
| } |
| |
| // If the attribute value has a fixed size and the value in the packet is shorter, |
| // then we overwrite only the start of the attribute value and keep the rest. |
| QByteArray value = packet.mid(3, valueLength); |
| if (attribute.minLength == attribute.maxLength && valueLength < attribute.minLength) |
| value += attribute.value.mid(valueLength, attribute.maxLength - valueLength); |
| |
| QLowEnergyCharacteristic characteristic; |
| QLowEnergyDescriptor descriptor; |
| updateLocalAttributeValue(handle, value, characteristic, descriptor); |
| |
| if (isRequest) { |
| const QByteArray response = QByteArray(1, ATT_OP_WRITE_RESPONSE); |
| sendPacket(response); |
| } |
| |
| if (characteristic.isValid()) { |
| emit characteristic.d_ptr->characteristicChanged(characteristic, value); |
| } else { |
| Q_ASSERT(descriptor.isValid()); |
| emit descriptor.d_ptr->descriptorWritten(descriptor, value); |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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) { |
| for (const WriteRequest &request : qAsConst(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)); |
| |
| for (const QLowEnergyCharacteristic &characteristic : qAsConst(characteristics)) |
| emit characteristic.d_ptr->characteristicChanged(characteristic, characteristic.value()); |
| for (const QLowEnergyDescriptor &descriptor : qAsConst(descriptors)) |
| emit descriptor.d_ptr->descriptorWritten(descriptor, descriptor.value()); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::sendNotification(QLowEnergyHandle handle) |
| { |
| sendNotificationOrIndication(ATT_OP_HANDLE_VAL_NOTIFICATION, handle); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::sendIndication(QLowEnergyHandle handle) |
| { |
| Q_ASSERT(!indicationInFlight); |
| indicationInFlight = true; |
| sendNotificationOrIndication(ATT_OP_HANDLE_VAL_INDICATION, handle); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::sendNextIndication() |
| { |
| if (!scheduledIndications.isEmpty()) |
| sendIndication(scheduledIndications.takeFirst()); |
| } |
| |
| static QString nameOfRemoteCentral(const QBluetoothAddress &peerAddress, const QBluetoothAddress &localAdapter) |
| { |
| const QString peerAddressString = peerAddress.toString(); |
| if (isBluez5()) { |
| OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"), |
| QStringLiteral("/"), |
| QDBusConnection::systemBus()); |
| QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects(); |
| reply.waitForFinished(); |
| if (reply.isError()) |
| return QString(); |
| |
| ManagedObjectList managedObjectList = reply.value(); |
| for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) { |
| const InterfaceList &ifaceList = it.value(); |
| |
| for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) { |
| const QString &iface = jt.key(); |
| const QVariantMap &ifaceValues = jt.value(); |
| |
| if (iface == QStringLiteral("org.bluez.Device1")) { |
| if (ifaceValues.value(QStringLiteral("Address")).toString() == peerAddressString) |
| return ifaceValues.value(QStringLiteral("Alias")).toString(); |
| } |
| } |
| } |
| return QString(); |
| } else { |
| OrgBluezManagerInterface manager(QStringLiteral("org.bluez"), QStringLiteral("/"), |
| QDBusConnection::systemBus()); |
| |
| QDBusPendingReply<QDBusObjectPath> reply = manager.FindAdapter(localAdapter.toString()); |
| reply.waitForFinished(); |
| if (reply.isError()) |
| return QString(); |
| |
| OrgBluezAdapterInterface adapter(QStringLiteral("org.bluez"), reply.value().path(), |
| QDBusConnection::systemBus()); |
| |
| QDBusPendingReply<QDBusObjectPath> deviceObjectPath = adapter.FindDevice(peerAddressString); |
| deviceObjectPath.waitForFinished(); |
| if (deviceObjectPath.isError()) { |
| if (deviceObjectPath.error().name() != QStringLiteral("org.bluez.Error.DoesNotExist")) |
| return QString(); |
| |
| deviceObjectPath = adapter.CreateDevice(peerAddressString); |
| deviceObjectPath.waitForFinished(); |
| if (deviceObjectPath.isError()) |
| return QString(); |
| } |
| |
| OrgBluezDeviceInterface device(QStringLiteral("org.bluez"), deviceObjectPath.value().path(), |
| QDBusConnection::systemBus()); |
| |
| QDBusPendingReply<QVariantMap> properties = device.GetProperties(); |
| properties.waitForFinished(); |
| if (properties.isError()) |
| return QString(); |
| |
| return properties.value().value(QStringLiteral("Alias")).toString(); |
| } |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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)); |
| remoteName = nameOfRemoteCentral(remoteDevice, localAdapter); |
| qCDebug(QT_BT_BLUEZ) << "GATT connection from device" << remoteDevice << remoteName; |
| |
| if (connectionHandle == 0) |
| qCWarning(QT_BT_BLUEZ) << "Received client connection, but no connection complete event"; |
| |
| if (l2cpSocket) { |
| disconnect(l2cpSocket); |
| if (l2cpSocket->isOpen()) |
| l2cpSocket->close(); |
| |
| l2cpSocket->deleteLater(); |
| l2cpSocket = nullptr; |
| } |
| closeServerSocket(); |
| |
| QBluetoothSocketPrivateBluez *rawSocketPrivate = new QBluetoothSocketPrivateBluez(); |
| l2cpSocket = new QBluetoothSocket( |
| rawSocketPrivate, QBluetoothServiceInfo::L2capProtocol, this); |
| connect(l2cpSocket, &QBluetoothSocket::disconnected, |
| this, &QLowEnergyControllerPrivateBluez::l2cpDisconnected); |
| connect(l2cpSocket, static_cast<void (QBluetoothSocket::*)(QBluetoothSocket::SocketError)> |
| (&QBluetoothSocket::error), this, &QLowEnergyControllerPrivateBluez::l2cpErrorChanged); |
| connect(l2cpSocket, &QIODevice::readyRead, this, &QLowEnergyControllerPrivateBluez::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(); |
| loadSigningDataIfNecessary(RemoteSigningKey); |
| |
| Q_Q(QLowEnergyController); |
| setState(QLowEnergyController::ConnectedState); |
| emit q->connected(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::closeServerSocket() |
| { |
| if (!serverSocketNotifier) |
| return; |
| serverSocketNotifier->disconnect(); |
| close(serverSocketNotifier->socket()); |
| serverSocketNotifier->deleteLater(); |
| serverSocketNotifier = nullptr; |
| } |
| |
| bool QLowEnergyControllerPrivateBluez::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<QLowEnergyControllerPrivateBluez::TempClientConfigurationData> QLowEnergyControllerPrivateBluez::gatherClientConfigData() |
| { |
| QVector<TempClientConfigurationData> data; |
| for (const auto &service : qAsConst(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 QLowEnergyControllerPrivateBluez::storeClientConfigurations() |
| { |
| if (!isBonded()) { |
| clientConfigData.remove(remoteDevice.toUInt64()); |
| return; |
| } |
| QVector<ClientConfigurationData> clientConfigs; |
| const QVector<TempClientConfigurationData> &tempConfigList = gatherClientConfigData(); |
| for (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 QLowEnergyControllerPrivateBluez::restoreClientConfigurations() |
| { |
| const QVector<TempClientConfigurationData> &tempConfigList = gatherClientConfigData(); |
| const QVector<ClientConfigurationData> &restoredClientConfigs = isBonded() |
| ? clientConfigData.value(remoteDevice.toUInt64()) : QVector<ClientConfigurationData>(); |
| QVector<QLowEnergyHandle> notifications; |
| for (const auto &tempConfigData : tempConfigList) { |
| bool wasRestored = false; |
| for (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; |
| } |
| |
| for (const QLowEnergyHandle handle : qAsConst(notifications)) |
| sendNotification(handle); |
| sendNextIndication(); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::loadSigningDataIfNecessary(SigningKeyType keyType) |
| { |
| const auto signingDataIt = signingData.constFind(remoteDevice.toUInt64()); |
| if (signingDataIt != signingData.constEnd()) |
| return; // We are up to date for this device. |
| const QString settingsFilePath = keySettingsFilePath(); |
| if (!QFileInfo(settingsFilePath).exists()) { |
| qCDebug(QT_BT_BLUEZ) << "No settings found for peer device."; |
| return; |
| } |
| QSettings settings(settingsFilePath, QSettings::IniFormat); |
| const QString group = signingKeySettingsGroup(keyType); |
| settings.beginGroup(group); |
| const QByteArray keyString = settings.value(QLatin1String("Key")).toByteArray(); |
| if (keyString.isEmpty()) { |
| qCDebug(QT_BT_BLUEZ) << "Group" << group << "not found in settings file"; |
| return; |
| } |
| const QByteArray keyData = QByteArray::fromHex(keyString); |
| if (keyData.count() != int(sizeof(quint128))) { |
| qCWarning(QT_BT_BLUEZ) << "Signing key in settings file has invalid size" |
| << keyString.count(); |
| return; |
| } |
| qCDebug(QT_BT_BLUEZ) << "CSRK of peer device is" << keyString; |
| const quint32 counter = settings.value(QLatin1String("Counter"), 0).toUInt(); |
| quint128 csrk; |
| using namespace std; |
| memcpy(csrk.data, keyData.constData(), keyData.count()); |
| signingData.insert(remoteDevice.toUInt64(), SigningData(csrk, counter - 1)); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::storeSignCounter(SigningKeyType keyType) const |
| { |
| const auto signingDataIt = signingData.constFind(remoteDevice.toUInt64()); |
| if (signingDataIt == signingData.constEnd()) |
| return; |
| const QString settingsFilePath = keySettingsFilePath(); |
| if (!QFileInfo(settingsFilePath).exists()) |
| return; |
| QSettings settings(settingsFilePath, QSettings::IniFormat); |
| if (!settings.isWritable()) |
| return; |
| settings.beginGroup(signingKeySettingsGroup(keyType)); |
| const QString counterKey = QLatin1String("Counter"); |
| if (!settings.allKeys().contains(counterKey)) |
| return; |
| const quint32 counterValue = signingDataIt.value().counter + 1; |
| if (counterValue == settings.value(counterKey).toUInt()) |
| return; |
| settings.setValue(counterKey, counterValue); |
| } |
| |
| QString QLowEnergyControllerPrivateBluez::signingKeySettingsGroup(SigningKeyType keyType) const |
| { |
| return QLatin1String(keyType == LocalSigningKey ? "LocalSignatureKey" : "RemoteSignatureKey"); |
| } |
| |
| QString QLowEnergyControllerPrivateBluez::keySettingsFilePath() const |
| { |
| return QString::fromLatin1("/var/lib/bluetooth/%1/%2/info") |
| .arg(localAdapter.toString(), remoteDevice.toString()); |
| } |
| |
| static QByteArray uuidToByteArray(const QBluetoothUuid &uuid) |
| { |
| QByteArray ba; |
| if (uuid.minimumSize() == 2) { |
| ba.resize(2); |
| putBtData(uuid.toUInt16(), ba.data()); |
| } else { |
| ba.resize(16); |
| quint128 hostOrder; |
| quint128 qtUuidOrder = uuid.toUInt128(); |
| ntoh128(&qtUuidOrder, &hostOrder); |
| putBtData(hostOrder, ba.data()); |
| } |
| return ba; |
| } |
| |
| void QLowEnergyControllerPrivateBluez::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; |
| const QList<QLowEnergyService *> includedServices = service.includedServices(); |
| for (const QLowEnergyService * const 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; |
| } |
| const QList<QLowEnergyCharacteristicData> characteristics = service.characteristics(); |
| for (const QLowEnergyCharacteristicData &cd : 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; |
| |
| const QList<QLowEnergyDescriptorData> descriptors = cd.descriptors(); |
| for (const QLowEnergyDescriptorData &dd : 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 QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::ensureUniformUuidSizes(QVector<Attribute> &attributes) |
| { |
| ensureUniformAttributes(attributes, |
| [](const Attribute &attr) { return getUuidSize(attr.type); }); |
| } |
| |
| void QLowEnergyControllerPrivateBluez::ensureUniformValueSizes(QVector<Attribute> &attributes) |
| { |
| ensureUniformAttributes(attributes, |
| [](const Attribute &attr) { return attr.value.count(); }); |
| } |
| |
| QVector<QLowEnergyControllerPrivateBluez::Attribute> QLowEnergyControllerPrivateBluez::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 QLowEnergyControllerPrivateBluez::checkPermissions(const Attribute &attr, |
| QLowEnergyCharacteristic::PropertyType type) |
| { |
| const bool isReadAccess = type == QLowEnergyCharacteristic::Read; |
| const bool isWriteCommand = type == QLowEnergyCharacteristic::WriteNoResponse; |
| const bool isWriteAccess = type == QLowEnergyCharacteristic::Write |
| || type == QLowEnergyCharacteristic::WriteSigned |
| || isWriteCommand; |
| Q_ASSERT(isReadAccess || isWriteAccess); |
| if (!(attr.properties & type)) { |
| if (isReadAccess) |
| return ATT_ERROR_READ_NOT_PERM; |
| |
| // The spec says: If an attribute requires a signed write, then a non-signed write command |
| // can also be used if the link is encrypted. |
| const bool unsignedWriteOk = isWriteCommand |
| && (attr.properties & QLowEnergyCharacteristic::WriteSigned) |
| && securityLevel() >= BT_SECURITY_MEDIUM; |
| if (!unsignedWriteOk) |
| return ATT_ERROR_WRITE_NOT_PERM; |
| } |
| const AttAccessConstraints constraints = isReadAccess |
| ? attr.readConstraints : attr.writeConstraints; |
| if (constraints.testFlag(AttAuthorizationRequired)) |
| return ATT_ERROR_INSUF_AUTHORIZATION; // TODO: emit signal (and offer authorization function)? |
| if (constraints.testFlag(AttEncryptionRequired) && securityLevel() < BT_SECURITY_MEDIUM) |
| return ATT_ERROR_INSUF_ENCRYPTION; |
| if (constraints.testFlag(AttAuthenticationRequired) && securityLevel() < BT_SECURITY_HIGH) |
| return ATT_ERROR_INSUF_AUTHENTICATION; |
| if (false) |
| return ATT_ERROR_INSUF_ENCR_KEY_SIZE; |
| return 0; |
| } |
| |
| int QLowEnergyControllerPrivateBluez::checkReadPermissions(const Attribute &attr) |
| { |
| return checkPermissions(attr, QLowEnergyCharacteristic::Read); |
| } |
| |
| int QLowEnergyControllerPrivateBluez::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; |
| } |
| |
| bool QLowEnergyControllerPrivateBluez::verifyMac(const QByteArray &message, const quint128 &csrk, |
| quint32 signCounter, quint64 expectedMac) |
| { |
| if (!cmacCalculator) |
| cmacCalculator = new LeCmacCalculator; |
| return cmacCalculator->verify(LeCmacCalculator::createFullMessage(message, signCounter), csrk, |
| expectedMac); |
| } |
| |
| QT_END_NAMESPACE |