| 1 | // Copyright (C) 2022 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include "qleadvertiser_bluezdbus_p.h" | 
| 5 | #include "bluez/leadvertisement1_p.h" | 
| 6 | #include "bluez/leadvertisingmanager1_p.h" | 
| 7 | #include "bluez/bluez5_helper_p.h" | 
| 8 |  | 
| 9 | #include <QtCore/QtMinMax> | 
| 10 | #include <QtCore/QLoggingCategory> | 
| 11 |  | 
| 12 | QT_BEGIN_NAMESPACE | 
| 13 |  | 
| 14 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) | 
| 15 |  | 
| 16 | using namespace Qt::StringLiterals; | 
| 17 |  | 
| 18 | // The advertisement dbus object path is freely definable, use as prefix | 
| 19 | static constexpr auto advObjectPathTemplate{"/qt/btle/advertisement/%1%2/%3"_L1 }; | 
| 20 | static constexpr auto bluezService{"org.bluez"_L1 }; | 
| 21 | static constexpr auto bluezErrorFailed{"org.bluez.Error.Failed"_L1 }; | 
| 22 |  | 
| 23 | // From bluez API documentation | 
| 24 | static constexpr auto advDataTXPower{"tx-power"_L1 }; | 
| 25 | static constexpr auto advDataTypePeripheral{"peripheral"_L1 }; | 
| 26 | static constexpr auto advDataTypeBroadcast{"broadcast"_L1 }; | 
| 27 | static constexpr quint16 advDataMinIntervalMs{20}; | 
| 28 | static constexpr quint16 advDataMaxIntervalMs{10485}; | 
| 29 |  | 
| 30 |  | 
| 31 | QLeDBusAdvertiser::QLeDBusAdvertiser(const QLowEnergyAdvertisingParameters ¶ms, | 
| 32 |                                      const QLowEnergyAdvertisingData &advertisingData, | 
| 33 |                                      const QLowEnergyAdvertisingData &scanResponseData, | 
| 34 |                                      const QString &hostAdapterPath, | 
| 35 |                                      QObject* parent) | 
| 36 |     : QObject(parent), | 
| 37 |       m_advParams(params), | 
| 38 |       m_advData(advertisingData), | 
| 39 |       m_advObjectPath(QString(advObjectPathTemplate). | 
| 40 |                       arg(a: sanitizeNameForDBus(text: QCoreApplication::applicationName())). | 
| 41 |                       arg(a: QCoreApplication::applicationPid()). | 
| 42 |                       arg(a: QRandomGenerator::global()->generate())), | 
| 43 |       m_advDataDBus(new OrgBluezLEAdvertisement1Adaptor(this)), | 
| 44 |       m_advManager(new OrgBluezLEAdvertisingManager1Interface(bluezService, hostAdapterPath, | 
| 45 |                                                               QDBusConnection::systemBus(), this)) | 
| 46 | { | 
| 47 |     // Bluez DBus API doesn't allow distinguishing between advertisement and scan response data; | 
| 48 |     // consolidate the two if they differ. | 
| 49 |     // Union of service UUIDs: | 
| 50 |     if (scanResponseData.services() != advertisingData.services()) { | 
| 51 |         QList<QBluetoothUuid> services = advertisingData.services(); | 
| 52 |         for (const auto &service: scanResponseData.services()) { | 
| 53 |             if (!services.contains(t: service)) | 
| 54 |                 services.append(t: service); | 
| 55 |         } | 
| 56 |         m_advData.setServices(services); | 
| 57 |     } | 
| 58 |     // Scan response is given precedence with rest of the data | 
| 59 |     if (!scanResponseData.localName().isEmpty()) | 
| 60 |         m_advData.setLocalName(scanResponseData.localName()); | 
| 61 |     if (scanResponseData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) { | 
| 62 |         m_advData.setManufacturerData(id: scanResponseData.manufacturerId(), | 
| 63 |                                       data: scanResponseData.manufacturerData()); | 
| 64 |     } | 
| 65 |     if (scanResponseData.includePowerLevel()) | 
| 66 |         m_advData.setIncludePowerLevel(true); | 
| 67 |  | 
| 68 |     setDataForDBus(); | 
| 69 | } | 
| 70 |  | 
| 71 | QLeDBusAdvertiser::~QLeDBusAdvertiser() | 
| 72 | { | 
| 73 |     stopAdvertising(); | 
| 74 | } | 
| 75 |  | 
| 76 | // This function parses the advertising data provided by the application and | 
| 77 | // populates the dbus adaptor with it. DBus will ask the data from the adaptor when | 
| 78 | // the advertisement is later registered (started) | 
| 79 | void QLeDBusAdvertiser::setDataForDBus() | 
| 80 | { | 
| 81 |     setAdvertisingParamsForDBus(); | 
| 82 |     setAdvertisementDataForDBus(); | 
| 83 | } | 
| 84 |  | 
| 85 | void QLeDBusAdvertiser::setAdvertisingParamsForDBus() | 
| 86 | { | 
| 87 |     // Whitelist and filter policy | 
| 88 |     if (!m_advParams.whiteList().isEmpty()) | 
| 89 |         qCWarning(QT_BT_BLUEZ) << "White lists and filter policies not supported, ignoring" ; | 
| 90 |  | 
| 91 |     // Legacy advertising mode mapped to GAP role (peripheral vs broadcast) | 
| 92 |     switch (m_advParams.mode()) | 
| 93 |     { | 
| 94 |     case QLowEnergyAdvertisingParameters::AdvScanInd: | 
| 95 |     case QLowEnergyAdvertisingParameters::AdvNonConnInd: | 
| 96 |         m_advDataDBus->setType(advDataTypeBroadcast); | 
| 97 |         break; | 
| 98 |     case QLowEnergyAdvertisingParameters::AdvInd: | 
| 99 |     default: | 
| 100 |         m_advDataDBus->setType(advDataTypePeripheral); | 
| 101 |     } | 
| 102 |  | 
| 103 |     // Advertisement interval (min max in milliseconds). Ensure the values fit the range bluez | 
| 104 |     // allows. The max >= min is guaranteed by QLowEnergyAdvertisingParameters::setInterval(). | 
| 105 |     // Note: Bluez reads these values but at the time of this writing it marks this feature | 
| 106 |     // as 'experimental' | 
| 107 |     m_advDataDBus->setMinInterval(qBound(min: advDataMinIntervalMs, | 
| 108 |                                          val: quint16(m_advParams.minimumInterval()), | 
| 109 |                                          max: advDataMaxIntervalMs)); | 
| 110 |     m_advDataDBus->setMaxInterval(qBound(min: advDataMinIntervalMs, | 
| 111 |                                          val: quint16(m_advParams.maximumInterval()), | 
| 112 |                                          max: advDataMaxIntervalMs)); | 
| 113 | } | 
| 114 |  | 
| 115 | void QLeDBusAdvertiser::setAdvertisementDataForDBus() | 
| 116 | { | 
| 117 |     // We don't calculate the advertisement length to guard for too long advertisements. | 
| 118 |     // There isn't adequate control and visibility on the advertisement for that. | 
| 119 |     // - We don't know the max length (legacy or extended advertising) | 
| 120 |     // - Bluez may truncate some of the fields on its own, making calculus here imprecise | 
| 121 |     // - Scan response may or may not be used to offload some of the data | 
| 122 |  | 
| 123 |     // Include the power level if requested and dbus supports it | 
| 124 |     const auto supportedIncludes = m_advManager->supportedIncludes(); | 
| 125 |     if (m_advData.includePowerLevel() && supportedIncludes.contains(str: advDataTXPower)) | 
| 126 |         m_advDataDBus->setIncludes({advDataTXPower}); | 
| 127 |  | 
| 128 |     // Set the application provided name (valid to be empty). | 
| 129 |     // For clarity: bluez also has "local-name" system include that could be set if no local | 
| 130 |     // name is provided. However that would require that the LocalName DBus property would | 
| 131 |     // not exist. Existing LocalName property when 'local-name' is included leads to an | 
| 132 |     // advertisement error. | 
| 133 |     m_advDataDBus->setLocalName(m_advData.localName()); | 
| 134 |  | 
| 135 |     // Service UUIDs | 
| 136 |     if (!m_advData.services().isEmpty()) { | 
| 137 |         QStringList serviceUUIDList; | 
| 138 |         for (const auto& service: m_advData.services()) | 
| 139 |             serviceUUIDList << service.toString(mode: QUuid::StringFormat::WithoutBraces); | 
| 140 |         m_advDataDBus->setServiceUUIDs(serviceUUIDList); | 
| 141 |     } | 
| 142 |  | 
| 143 |     // Manufacturer data | 
| 144 |     if (m_advData.manufacturerId() != QLowEnergyAdvertisingData::invalidManufacturerId()) { | 
| 145 |         m_advDataDBus->setManufacturerData({ | 
| 146 |                         {m_advData.manufacturerId(), QDBusVariant(m_advData.manufacturerData())}}); | 
| 147 |     } | 
| 148 |  | 
| 149 |     // Discoverability | 
| 150 |     if (m_advDataDBus->type() == advDataTypePeripheral) { | 
| 151 |         m_advDataDBus->setDiscoverable(m_advData.discoverability() | 
| 152 |                                    != QLowEnergyAdvertisingData::DiscoverabilityNone); | 
| 153 |     } else { | 
| 154 |         qCDebug(QT_BT_BLUEZ) << "Ignoring advertisement discoverability in broadcast mode" ; | 
| 155 |     } | 
| 156 |  | 
| 157 |     // Raw data | 
| 158 |     if (!m_advData.rawData().isEmpty()) | 
| 159 |         qCWarning(QT_BT_BLUEZ) <<  "Raw advertisement data not supported, ignoring" ; | 
| 160 | } | 
| 161 |  | 
| 162 | void QLeDBusAdvertiser::startAdvertising() | 
| 163 | { | 
| 164 |     qCDebug(QT_BT_BLUEZ) << "Start advertising"  << m_advObjectPath << "on"  << m_advManager->path(); | 
| 165 |     if (m_advertising) { | 
| 166 |         qCWarning(QT_BT_BLUEZ) << "Start tried while already advertising" ; | 
| 167 |         return; | 
| 168 |     } | 
| 169 |  | 
| 170 |     if (!QDBusConnection::systemBus().registerObject(path: m_advObjectPath, object: m_advDataDBus, | 
| 171 |                                                      options: QDBusConnection::ExportAllContents)) { | 
| 172 |         qCWarning(QT_BT_BLUEZ) << "Advertisement dbus object registration failed" ; | 
| 173 |         emit errorOccurred(); | 
| 174 |         return; | 
| 175 |     } | 
| 176 |  | 
| 177 |     // Register the advertisement which starts the actual advertising. | 
| 178 |     // We use call watcher here instead of waitForFinished() because DBus will | 
| 179 |     // call back our advertisement object (to read data) in this same thread => would block | 
| 180 |     auto reply = m_advManager->RegisterAdvertisement(advertisement: QDBusObjectPath(m_advObjectPath), options: {}); | 
| 181 |     QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); | 
| 182 |  | 
| 183 |     QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, | 
| 184 |                      slot: [this](QDBusPendingCallWatcher* watcher){ | 
| 185 |         QDBusPendingReply<> reply = *watcher; | 
| 186 |         if (reply.isError()) { | 
| 187 |             qCWarning(QT_BT_BLUEZ) << "Advertisement registration failed"  << reply.error(); | 
| 188 |             if (reply.error().name() == bluezErrorFailed) | 
| 189 |                 qCDebug(QT_BT_BLUEZ) << "Advertisement could've been too large" ; | 
| 190 |             QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath); | 
| 191 |             emit errorOccurred(); | 
| 192 |         } else { | 
| 193 |             qCDebug(QT_BT_BLUEZ) << "Advertisement started successfully" ; | 
| 194 |             m_advertising = true; | 
| 195 |         } | 
| 196 |         watcher->deleteLater(); | 
| 197 |     }); | 
| 198 | } | 
| 199 |  | 
| 200 | void QLeDBusAdvertiser::stopAdvertising() | 
| 201 | { | 
| 202 |     if (!m_advertising) | 
| 203 |         return; | 
| 204 |  | 
| 205 |     m_advertising = false; | 
| 206 |     auto reply = m_advManager->UnregisterAdvertisement(advertisement: QDBusObjectPath(m_advObjectPath)); | 
| 207 |     reply.waitForFinished(); | 
| 208 |     if (reply.isError()) | 
| 209 |         qCWarning(QT_BT_BLUEZ) << "Error in unregistering advertisement"  << reply.error(); | 
| 210 |     else | 
| 211 |         qCDebug(QT_BT_BLUEZ) << "Advertisement unregistered successfully" ; | 
| 212 |     QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath); | 
| 213 | } | 
| 214 |  | 
| 215 | // Called by Bluez when the advertisement has been removed (org.bluez.LEAdvertisement1.Release) | 
| 216 | void QLeDBusAdvertiser::Release() | 
| 217 | { | 
| 218 |     qCDebug(QT_BT_BLUEZ) << "Advertisement"  << m_advObjectPath << "released"  | 
| 219 |                          << (m_advertising ? "unexpectedly"  : "" ); | 
| 220 |     if (m_advertising) { | 
| 221 |         // If we are advertising, it means the Release is unsolicited | 
| 222 |         // and handled as an advertisement error. No need to call UnregisterAdvertisement | 
| 223 |         m_advertising = false; | 
| 224 |         QDBusConnection::systemBus().unregisterObject(path: m_advObjectPath); | 
| 225 |         emit errorOccurred(); | 
| 226 |     } | 
| 227 | } | 
| 228 |  | 
| 229 | QT_END_NAMESPACE | 
| 230 |  |