// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "bluezperipheralobjects_p.h" #include "propertiesadaptor_p.h" #include "gattservice1adaptor_p.h" #include "gattcharacteristic1adaptor_p.h" #include "gattdescriptor1adaptor_p.h" #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) using namespace Qt::StringLiterals; using namespace QtBluetoothPrivate; // for D-Bus adaptors static constexpr auto characteristicPathTemplate{"%1/char%2"_L1}; static constexpr auto descriptorPathTemplate{"%1/desc%2"_L1}; static constexpr auto servicePathTemplate{"%1/service%2"_L1}; // The interface names and error values are from BlueZ "gatt-api" documentation static constexpr auto bluezServiceInterface{"org.bluez.GattService1"_L1}; static constexpr auto bluezCharacteristicInterface{"org.bluez.GattCharacteristic1"_L1}; static constexpr auto bluezDescriptorInterface{"org.bluez.GattDescriptor1"_L1}; static constexpr auto bluezErrorInvalidValueLength{"org.bluez.Error.InvalidValueLength"_L1}; static constexpr auto bluezErrorInvalidOffset{"org.bluez.Error.InvalidOffset"_L1}; static constexpr auto bluezErrorNotAuthorized{"org.bluez.Error.NotAuthorized"_L1}; // Bluetooth Core v5.3, 3.2.9, Vol 3, Part F static constexpr int maximumAttributeLength{512}; QtBluezPeripheralGattObject::QtBluezPeripheralGattObject(const QString& objectPath, const QString& uuid, QLowEnergyHandle handle, QObject* parent) : QObject(parent), objectPath(objectPath), uuid(uuid), handle(handle), propertiesAdaptor(new OrgFreedesktopDBusPropertiesAdaptor(this)) {} QtBluezPeripheralGattObject::~QtBluezPeripheralGattObject() { unregisterObject(); } bool QtBluezPeripheralGattObject::registerObject() { if (m_registered) return true; if (QDBusConnection::systemBus().registerObject(objectPath, this)) { qCDebug(QT_BT_BLUEZ) << "Registered object on DBus:" << objectPath << uuid; m_registered = true; return true; } else { qCWarning(QT_BT_BLUEZ) << "Failed to register object on DBus:" << objectPath << uuid; return false; } } void QtBluezPeripheralGattObject::unregisterObject() { if (!m_registered) return; QDBusConnection::systemBus().unregisterObject(objectPath); qCDebug(QT_BT_BLUEZ) << "Unregistered object on DBus:" << objectPath << uuid; m_registered = false; } void QtBluezPeripheralGattObject::accessEvent(const QVariantMap& options) { // Report this event for connection management purposes const auto remoteDevice = options.value("device"_L1).value().path(); if (!remoteDevice.isEmpty()) emit remoteDeviceAccessEvent(remoteDevice, options.value("mtu"_L1).toUInt()); } QtBluezPeripheralDescriptor::QtBluezPeripheralDescriptor( const QLowEnergyDescriptorData& descriptorData, const QString& characteristicPath, quint16 ordinal, QLowEnergyHandle handle, QLowEnergyHandle characteristicHandle, QObject* parent) : QtBluezPeripheralGattObject(descriptorPathTemplate.arg(characteristicPath).arg(ordinal), descriptorData.uuid().toString(QUuid::WithoutBraces), handle, parent), m_adaptor(new OrgBluezGattDescriptor1Adaptor(this)), m_characteristicPath(characteristicPath), m_characteristicHandle(characteristicHandle) { if (descriptorData.value().size() > maximumAttributeLength) { qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large, cropping it to" << maximumAttributeLength; m_value = descriptorData.value().sliced(0, maximumAttributeLength); } else { m_value = descriptorData.value(); } initializeFlags(descriptorData); } InterfaceList QtBluezPeripheralDescriptor::properties() const { InterfaceList properties; properties.insert(bluezDescriptorInterface, { {"UUID"_L1, uuid}, {"Characteristic"_L1, QDBusObjectPath(m_characteristicPath)}, {"Flags"_L1, m_flags} }); return properties; } // org.bluez.GattDescriptor1 // This function is invoked when remote device reads the value QByteArray QtBluezPeripheralDescriptor::ReadValue(const QVariantMap &options, QString& error) { accessEvent(options); // Offset is set by Bluez when the value size is more than MTU size. // Bluez deduces the value size from the first ReadValue. If the // received data size is larger than MTU, Bluez will take the first MTU bytes and // issue more ReadValue calls with the 'offset' set const quint16 offset = options.value("offset"_L1).toUInt(); const quint16 mtu = options.value("mtu"_L1).toUInt(); if (offset > m_value.length() - 1) { qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length(); error = bluezErrorInvalidOffset; return {}; } if (offset > 0) return m_value.mid(offset, mtu); else return m_value; } // org.bluez.GattDescriptor1 // This function is invoked when remote device writes a value QString QtBluezPeripheralDescriptor::WriteValue(const QByteArray &value, const QVariantMap &options) { accessEvent(options); if (options.value("prepare-authorize"_L1).toBool()) { // Qt API doesn't provide the means for application to authorize qCWarning(QT_BT_BLUEZ) << "Descriptor write requires authorization." << "The client device needs to be trusted beforehand"; return bluezErrorNotAuthorized; } if (value.size() > maximumAttributeLength) { qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size(); return bluezErrorInvalidValueLength; } m_value = value; emit valueUpdatedByRemote(m_characteristicHandle, handle, value); return {}; } // This function is called when the value has been updated locally (server-side) bool QtBluezPeripheralDescriptor::localValueUpdate(const QByteArray& value) { if (value.size() > maximumAttributeLength) { qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size(); return false; } m_value = value; return true; } void QtBluezPeripheralDescriptor::initializeFlags(const QLowEnergyDescriptorData& data) { // Flag tokens are from org.bluez.GattDescriptor1 documentation if (data.isReadable()) m_flags.append("read"_L1); if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) m_flags.append("encrypt-read"_L1); if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) m_flags.append("encrypt-authenticated-read"_L1); if (data.isWritable()) m_flags.append("write"_L1); if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) m_flags.append("encrypt-write"_L1); if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) m_flags.append("encrypt-authenticated-write"_L1); if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired) m_flags.append("authorize"_L1); if (m_flags.isEmpty()) { qCWarning(QT_BT_BLUEZ) << "Descriptor property flags not set" << uuid << "Peripheral may fail to register"; } } QtBluezPeripheralCharacteristic::QtBluezPeripheralCharacteristic( const QLowEnergyCharacteristicData& characteristicData, const QString& servicePath, quint16 ordinal, QLowEnergyHandle handle, QObject* parent) : QtBluezPeripheralGattObject(characteristicPathTemplate.arg(servicePath).arg(ordinal), characteristicData.uuid().toString(QUuid::WithoutBraces), handle, parent), m_adaptor(new OrgBluezGattCharacteristic1Adaptor(this)), m_servicePath(servicePath), m_minimumValueLength(std::min(characteristicData.minimumValueLength(), maximumAttributeLength)), m_maximumValueLength(std::min(characteristicData.maximumValueLength(), maximumAttributeLength)) { initializeFlags(characteristicData); initializeValue(characteristicData.value()); } InterfaceList QtBluezPeripheralCharacteristic::properties() const { InterfaceList properties; properties.insert(bluezCharacteristicInterface, { {"UUID"_L1, uuid}, {"Service"_L1, QDBusObjectPath(m_servicePath)}, {"Flags"_L1, m_flags} }); return properties; } // org.bluez.GattCharacteristic1 // This function is invoked when remote device reads the value QByteArray QtBluezPeripheralCharacteristic::ReadValue(const QVariantMap &options, QString& error) { accessEvent(options); // Offset is set by Bluez when the value size is more than MTU size. // Bluez deduces the value size from the first ReadValue. If the // received data size is larger than MTU, Bluez will take the first MTU bytes and // issue more ReadValue calls with the 'offset' set const quint16 offset = options.value("offset"_L1).toUInt(); const quint16 mtu = options.value("mtu"_L1).toUInt(); if (offset > m_value.length() - 1) { qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length(); error = bluezErrorInvalidOffset; return {}; } if (offset > 0) return m_value.mid(offset, mtu); else return m_value; } // org.bluez.GattCharacteristic1 // This function is invoked when remote device writes a value QString QtBluezPeripheralCharacteristic::WriteValue(const QByteArray &value, const QVariantMap &options) { accessEvent(options); if (options.value("prepare-authorize"_L1).toBool()) { // Qt API doesn't provide the means for application to authorize qCWarning(QT_BT_BLUEZ) << "Characteristic write requires authorization." << "The client device needs to be trusted beforehand"; return bluezErrorNotAuthorized; } if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) { qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size() << "min:" << m_minimumValueLength << "max:" << m_maximumValueLength; return bluezErrorInvalidValueLength; } m_value = value; emit valueUpdatedByRemote(handle, value); return {}; } // This function is called when the value has been updated locally (server-side) bool QtBluezPeripheralCharacteristic::localValueUpdate(const QByteArray& value) { if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) { qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size() << "min:" << m_minimumValueLength << "max:" << m_maximumValueLength; return false; } m_value = value; if (m_notifying) { emit propertiesAdaptor->PropertiesChanged( bluezCharacteristicInterface, {{"Value"_L1, m_value}}, {}); } return true; } // org.bluez.GattCharacteristic1 // These are called when remote client enables or disables NTF/IND void QtBluezPeripheralCharacteristic::StartNotify() { qCDebug(QT_BT_BLUEZ) << "NTF or IND enabled for characteristic" << uuid; m_notifying = true; } void QtBluezPeripheralCharacteristic::StopNotify() { qCDebug(QT_BT_BLUEZ) << "NTF or IND disabled for characteristic" << uuid; m_notifying = false; } void QtBluezPeripheralCharacteristic::initializeValue(const QByteArray& value) { const auto valueSize = value.size(); if (valueSize < m_minimumValueLength || valueSize > m_maximumValueLength) { qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << valueSize << "min:" << m_minimumValueLength << "max:" << m_maximumValueLength; m_value = QByteArray(m_minimumValueLength, 0); } else { m_value = value; } } void QtBluezPeripheralCharacteristic::initializeFlags(const QLowEnergyCharacteristicData& data) { // Flag tokens are from org.bluez.GattCharacteristic1 documentation if (data.properties() & QLowEnergyCharacteristic::PropertyType::Broadcasting) m_flags.append("broadcast"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteNoResponse) m_flags.append("write-without-response"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::Read) m_flags.append("read"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::Write) m_flags.append("write"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::Notify) m_flags.append("notify"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::Indicate) m_flags.append("indicate"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteSigned) m_flags.append("authenticated-signed-writes"_L1); if (data.properties() & QLowEnergyCharacteristic::PropertyType::ExtendedProperty) { // If extended properties property is set, check if we have the descriptor // describing them. Bluez will generate the actual descriptor based on these // flags. For clarity: the 'extended-properties' token mentioned in the Bluez // API is implied by these flags. for (const auto& descriptor : data.descriptors()) { // Core Bluetooth v5.3 Vol 3, Part G, 3.3.3.1 if (descriptor.uuid() == QBluetoothUuid::DescriptorType::CharacteristicExtendedProperties && descriptor.value().size() == 2) { const auto properties = descriptor.value().at(0); if (properties & 0x01) m_flags.append("reliable-write"_L1); if (properties & 0x02) m_flags.append("writable-auxiliaries"_L1); } } } if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) m_flags.append("encrypt-read"_L1); if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) m_flags.append("encrypt-authenticated-read"_L1); if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) m_flags.append("encrypt-write"_L1); if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) m_flags.append("encrypt-authenticated-write"_L1); if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired) m_flags.append("authorize"_L1); if (m_flags.isEmpty()) { qCWarning(QT_BT_BLUEZ) << "Characteristic property flags not set" << uuid << "Peripheral may fail to register"; } } QtBluezPeripheralService::QtBluezPeripheralService(const QLowEnergyServiceData &serviceData, const QString& applicationPath, quint16 ordinal, QLowEnergyHandle handle, QObject* parent) : QtBluezPeripheralGattObject(servicePathTemplate.arg(applicationPath).arg(ordinal), serviceData.uuid().toString(QUuid::WithoutBraces), handle, parent), m_isPrimary(serviceData.type() == QLowEnergyServiceData::ServiceTypePrimary), m_adaptor(new OrgBluezGattService1Adaptor(this)) { } void QtBluezPeripheralService::addIncludedService(const QString& objectPath) { qCDebug(QT_BT_BLUEZ) << "Adding included service" << objectPath << "for" << uuid; m_includedServices.append(QDBusObjectPath(objectPath)); } InterfaceList QtBluezPeripheralService::properties() const { InterfaceList interfaces; interfaces.insert(bluezServiceInterface,{ {"UUID"_L1, uuid}, {"Primary"_L1, m_isPrimary}, {"Includes"_L1, QVariant::fromValue(m_includedServices)} }); return interfaces; }; QT_END_NAMESPACE #include "moc_bluezperipheralobjects_p.cpp"