// 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 "qleadvertiser_bluezdbus_p.h" #include "bluez/leadvertisement1_p.h" #include "bluez/leadvertisingmanager1_p.h" #include "bluez/bluez5_helper_p.h" #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) using namespace Qt::StringLiterals; // The advertisement dbus object path is freely definable, use as prefix static constexpr auto advObjectPathTemplate{"/qt/btle/advertisement/%1%2/%3"_L1}; static constexpr auto bluezService{"org.bluez"_L1}; static constexpr auto bluezErrorFailed{"org.bluez.Error.Failed"_L1}; // From bluez API documentation static constexpr auto advDataTXPower{"tx-power"_L1}; static constexpr auto advDataTypePeripheral{"peripheral"_L1}; static constexpr auto advDataTypeBroadcast{"broadcast"_L1}; static constexpr quint16 advDataMinIntervalMs{20}; static constexpr quint16 advDataMaxIntervalMs{10485}; QLeDBusAdvertiser::QLeDBusAdvertiser(const QLowEnergyAdvertisingParameters ¶ms, const QLowEnergyAdvertisingData &advertisingData, const QLowEnergyAdvertisingData &scanResponseData, const QString &hostAdapterPath, QObject* parent) : QObject(parent), m_advParams(params), m_advData(advertisingData), m_advObjectPath(QString(advObjectPathTemplate). arg(sanitizeNameForDBus(QCoreApplication::applicationName())). arg(QCoreApplication::applicationPid()). arg(QRandomGenerator::global()->generate())), m_advDataDBus(new OrgBluezLEAdvertisement1Adaptor(this)), m_advManager(new OrgBluezLEAdvertisingManager1Interface(bluezService, hostAdapterPath, QDBusConnection::systemBus(), this)) { // Bluez DBus API doesn't allow distinguishing between advertisement and scan response data; // consolidate the two if they differ. // Union of service UUIDs: if (scanResponseData.services() != advertisingData.services()) { QList services = advertisingData.services(); for (const auto &service: scanResponseData.services()) { if (!services.contains(service)) services.append(service); } m_advData.setServices(services); } // Scan response is given precedence with rest of the data if (!scanResponseData.localName().isEmpty()) m_advData.setLocalName(scanResponseData.localName()); if (scanResponseData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) { m_advData.setManufacturerData(scanResponseData.manufacturerId(), scanResponseData.manufacturerData()); } if (scanResponseData.includePowerLevel()) m_advData.setIncludePowerLevel(true); setDataForDBus(); } QLeDBusAdvertiser::~QLeDBusAdvertiser() { stopAdvertising(); } // This function parses the advertising data provided by the application and // populates the dbus adaptor with it. DBus will ask the data from the adaptor when // the advertisement is later registered (started) void QLeDBusAdvertiser::setDataForDBus() { setAdvertisingParamsForDBus(); setAdvertisementDataForDBus(); } void QLeDBusAdvertiser::setAdvertisingParamsForDBus() { // Whitelist and filter policy if (!m_advParams.whiteList().isEmpty()) qCWarning(QT_BT_BLUEZ) << "White lists and filter policies not supported, ignoring"; // Legacy advertising mode mapped to GAP role (peripheral vs broadcast) switch (m_advParams.mode()) { case QLowEnergyAdvertisingParameters::AdvScanInd: case QLowEnergyAdvertisingParameters::AdvNonConnInd: m_advDataDBus->setType(advDataTypeBroadcast); break; case QLowEnergyAdvertisingParameters::AdvInd: default: m_advDataDBus->setType(advDataTypePeripheral); } // Advertisement interval (min max in milliseconds). Ensure the values fit the range bluez // allows. The max >= min is guaranteed by QLowEnergyAdvertisingParameters::setInterval(). // Note: Bluez reads these values but at the time of this writing it marks this feature // as 'experimental' m_advDataDBus->setMinInterval(qBound(advDataMinIntervalMs, quint16(m_advParams.minimumInterval()), advDataMaxIntervalMs)); m_advDataDBus->setMaxInterval(qBound(advDataMinIntervalMs, quint16(m_advParams.maximumInterval()), advDataMaxIntervalMs)); } void QLeDBusAdvertiser::setAdvertisementDataForDBus() { // We don't calculate the advertisement length to guard for too long advertisements. // There isn't adequate control and visibility on the advertisement for that. // - We don't know the max length (legacy or extended advertising) // - Bluez may truncate some of the fields on its own, making calculus here imprecise // - Scan response may or may not be used to offload some of the data // Include the power level if requested and dbus supports it const auto supportedIncludes = m_advManager->supportedIncludes(); if (m_advData.includePowerLevel() && supportedIncludes.contains(advDataTXPower)) m_advDataDBus->setIncludes({advDataTXPower}); // Set the application provided name (valid to be empty). // For clarity: bluez also has "local-name" system include that could be set if no local // name is provided. However that would require that the LocalName DBus property would // not exist. Existing LocalName property when 'local-name' is included leads to an // advertisement error. m_advDataDBus->setLocalName(m_advData.localName()); // Service UUIDs if (!m_advData.services().isEmpty()) { QStringList serviceUUIDList; for (const auto& service: m_advData.services()) serviceUUIDList << service.toString(QUuid::StringFormat::WithoutBraces); m_advDataDBus->setServiceUUIDs(serviceUUIDList); } // Manufacturer data if (m_advData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) { m_advDataDBus->setManufacturerData({ {m_advData.manufacturerId(), QDBusVariant(m_advData.manufacturerData())}}); } // Discoverability if (m_advDataDBus->type() == advDataTypePeripheral) { m_advDataDBus->setDiscoverable(m_advData.discoverability() != QLowEnergyAdvertisingData::DiscoverabilityNone); } else { qCDebug(QT_BT_BLUEZ) << "Ignoring advertisement discoverability in broadcast mode"; } // Raw data if (!m_advData.rawData().isEmpty()) qCWarning(QT_BT_BLUEZ) << "Raw advertisement data not supported, ignoring"; } void QLeDBusAdvertiser::startAdvertising() { qCDebug(QT_BT_BLUEZ) << "Start advertising" << m_advObjectPath << "on" << m_advManager->path(); if (m_advertising) { qCWarning(QT_BT_BLUEZ) << "Start tried while already advertising"; return; } if (!QDBusConnection::systemBus().registerObject(m_advObjectPath, m_advDataDBus, QDBusConnection::ExportAllContents)) { qCWarning(QT_BT_BLUEZ) << "Advertisement dbus object registration failed"; emit errorOccurred(); return; } // Register the advertisement which starts the actual advertising. // We use call watcher here instead of waitForFinished() because DBus will // call back our advertisement object (to read data) in this same thread => would block auto reply = m_advManager->RegisterAdvertisement(QDBusObjectPath(m_advObjectPath), {}); QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher* watcher){ QDBusPendingReply<> reply = *watcher; if (reply.isError()) { qCWarning(QT_BT_BLUEZ) << "Advertisement registration failed" << reply.error(); if (reply.error().name() == bluezErrorFailed) qCDebug(QT_BT_BLUEZ) << "Advertisement could've been too large"; QDBusConnection::systemBus().unregisterObject(m_advObjectPath); emit errorOccurred(); } else { qCDebug(QT_BT_BLUEZ) << "Advertisement started successfully"; m_advertising = true; } watcher->deleteLater(); }); } void QLeDBusAdvertiser::stopAdvertising() { if (!m_advertising) return; m_advertising = false; auto reply = m_advManager->UnregisterAdvertisement(QDBusObjectPath(m_advObjectPath)); reply.waitForFinished(); if (reply.isError()) qCWarning(QT_BT_BLUEZ) << "Error in unregistering advertisement" << reply.error(); else qCDebug(QT_BT_BLUEZ) << "Advertisement unregistered successfully"; QDBusConnection::systemBus().unregisterObject(m_advObjectPath); } // Called by Bluez when the advertisement has been removed (org.bluez.LEAdvertisement1.Release) void QLeDBusAdvertiser::Release() { qCDebug(QT_BT_BLUEZ) << "Advertisement" << m_advObjectPath << "released" << (m_advertising ? "unexpectedly" : ""); if (m_advertising) { // If we are advertising, it means the Release is unsolicited // and handled as an advertisement error. No need to call UnregisterAdvertisement m_advertising = false; QDBusConnection::systemBus().unregisterObject(m_advObjectPath); emit errorOccurred(); } } QT_END_NAMESPACE