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
12QT_BEGIN_NAMESPACE
13
14Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
15
16using namespace Qt::StringLiterals;
17
18// The advertisement dbus object path is freely definable, use as prefix
19static constexpr auto advObjectPathTemplate{"/qt/btle/advertisement/%1%2/%3"_L1};
20static constexpr auto bluezService{"org.bluez"_L1};
21static constexpr auto bluezErrorFailed{"org.bluez.Error.Failed"_L1};
22
23// From bluez API documentation
24static constexpr auto advDataTXPower{"tx-power"_L1};
25static constexpr auto advDataTypePeripheral{"peripheral"_L1};
26static constexpr auto advDataTypeBroadcast{"broadcast"_L1};
27static constexpr quint16 advDataMinIntervalMs{20};
28static constexpr quint16 advDataMaxIntervalMs{10485};
29
30
31QLeDBusAdvertiser::QLeDBusAdvertiser(const QLowEnergyAdvertisingParameters &params,
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
71QLeDBusAdvertiser::~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)
79void QLeDBusAdvertiser::setDataForDBus()
80{
81 setAdvertisingParamsForDBus();
82 setAdvertisementDataForDBus();
83}
84
85void 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
115void 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
162void 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
200void 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)
216void 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
229QT_END_NAMESPACE
230

source code of qtconnectivity/src/bluetooth/qleadvertiser_bluezdbus.cpp